<?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: zerox80</title>
    <description>The latest articles on DEV Community by zerox80 (@zerox80).</description>
    <link>https://dev.to/zerox80</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%2F4229178%2F887d0b8a-3a57-45e5-8458-c350bc76884c.jpg</url>
      <title>DEV Community: zerox80</title>
      <link>https://dev.to/zerox80</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zerox80"/>
    <language>en</language>
    <item>
      <title>Before authentication: three bugs in Mullvad's gotatun</title>
      <dc:creator>zerox80</dc:creator>
      <pubDate>Thu, 05 Nov 2026 20:10:45 +0000</pubDate>
      <link>https://dev.to/zerox80/before-authentication-three-bugs-in-mullvads-gotatun-25ih</link>
      <guid>https://dev.to/zerox80/before-authentication-three-bugs-in-mullvads-gotatun-25ih</guid>
      <description>&lt;p&gt;WireGuard is known for staying silent towards packets it can't authenticate. But a userspace implementation has to receive a UDP datagram before it can decide whether to ignore it. That receive path is attack surface too.&lt;/p&gt;

&lt;p&gt;I found three bugs there in &lt;a href="https://github.com/mullvad/gotatun" rel="noopener noreferrer"&gt;gotatun&lt;/a&gt;, Mullvad's userspace WireGuard implementation in Rust. All three trigger before WireGuard authentication, and all three are fixed.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. An empty datagram kills the Linux receive task
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/mullvad/gotatun/security/advisories/GHSA-6gxq-8xp9-742r" rel="noopener noreferrer"&gt;GHSA-6gxq-8xp9-742r&lt;/a&gt; · High (CVSS 7.5) · fixed in 0.10.1&lt;/p&gt;

&lt;p&gt;On Linux, gotatun receives datagrams in batches with &lt;code&gt;recvmmsg&lt;/code&gt;. For each result it took the payload like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="nf"&gt;.iovs&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"we create exactly one IoSlice"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For an empty datagram, the iterator returns &lt;code&gt;None&lt;/code&gt;, and &lt;code&gt;expect&lt;/code&gt; panics. That panic ends the receive task that all peers on the device share. One zero-length UDP datagram to the listening port, and inbound traffic stops for every peer until the device is restarted or reconfigured. No peer key or secret is needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; an empty UDP datagram is perfectly legal. "This can't happen" in a receive path is a denial of service waiting for someone to send zero bytes.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. An oversized datagram stops reception on Windows
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/mullvad/gotatun/security/advisories/GHSA-fc57-wfhg-7c2w" rel="noopener noreferrer"&gt;GHSA-fc57-wfhg-7c2w&lt;/a&gt; · High (CVSS 7.5) · fixed in 0.10.0&lt;/p&gt;

&lt;p&gt;On Windows without the optional &lt;code&gt;windows-gro&lt;/code&gt; feature, gotatun receives into a 4096-byte buffer. If a datagram is larger, Windows returns &lt;code&gt;WSAEMSGSIZE&lt;/code&gt; (10040). The socket itself is fine after that, the next receive works.&lt;/p&gt;

&lt;p&gt;But gotatun passed the error up. The buffered receive task exited, and once the queued packets drained, the receive loop ended. It ended with &lt;code&gt;Ok(())&lt;/code&gt;, so nothing registered a fatal error and nothing restarted reception.&lt;/p&gt;

&lt;p&gt;I tested this on native Windows 11 with two connected devices over loopback. After a single 8192-byte datagram, the unpatched device did not deliver the next packet within five seconds. With my patch, traffic kept flowing in both directions. The patch was part of my report. It discards datagrams that fail with &lt;code&gt;WSAEMSGSIZE&lt;/code&gt; and keeps receiving:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="nf"&gt;.socket&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.recv_from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* deliver the datagram */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// The socket is still usable; skip the oversized datagram.&lt;/span&gt;
    &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="nf"&gt;.raw_os_error&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;WinSock&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;WSAEMSGSIZE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nn"&gt;tokio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;task&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;yield_now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; an error that is harmless for the socket was fatal for the task. And a task that exits with &lt;code&gt;Ok(())&lt;/code&gt; after failing is worse than one that crashes, because nobody notices.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. A rate limiter that never forgets
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/mullvad/gotatun/security/advisories/GHSA-m2rr-7rr7-vmp7" rel="noopener noreferrer"&gt;GHSA-m2rr-7rr7-vmp7&lt;/a&gt; · Medium · fixed in 0.10.1&lt;/p&gt;

&lt;p&gt;To protect against handshake floods, gotatun counts handshake packets per source IP. A packet only gets counted if its MAC1 is valid. But the MAC1 key is derived from the device's public key, so anyone who knows the public key can produce valid MAC1 values.&lt;/p&gt;

&lt;p&gt;The counters are cleared from the peer timer loop. That loop skips peers without an endpoint. If no peer has an endpoint, the cleanup never runs, and every new source IP adds an entry. On top of that, &lt;code&gt;HashMap::clear()&lt;/code&gt; keeps the allocated capacity, so even a reset doesn't give the memory back.&lt;/p&gt;

&lt;p&gt;This one needs traffic from many different source IPs, so network filtering and anti-spoofing make it much harder. It is rated Medium.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; cleanup that depends on an unrelated condition will eventually not run. And &lt;code&gt;clear()&lt;/code&gt; empties a map, it doesn't shrink it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What they have in common
&lt;/h2&gt;

&lt;p&gt;All three happen before the packet reaches authentication. A quick checklist for any UDP service:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What happens with a 0-byte datagram?&lt;/li&gt;
&lt;li&gt;What happens with a datagram larger than my buffer?&lt;/li&gt;
&lt;li&gt;What happens if a million different source IPs send me one packet each?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you use gotatun, update to 0.10.1 or later.&lt;/p&gt;

&lt;p&gt;Thanks to the Mullvad team for the quick fixes and the advisories.&lt;/p&gt;

</description>
      <category>security</category>
      <category>rust</category>
      <category>wireguard</category>
      <category>networking</category>
    </item>
    <item>
      <title>Two ways to stall quinn, Rust's QUIC library</title>
      <dc:creator>zerox80</dc:creator>
      <pubDate>Thu, 05 Nov 2026 20:10:34 +0000</pubDate>
      <link>https://dev.to/zerox80/two-ways-to-stall-quinn-rusts-quic-library-3150</link>
      <guid>https://dev.to/zerox80/two-ways-to-stall-quinn-rusts-quic-library-3150</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/quinn-rs/quinn" rel="noopener noreferrer"&gt;quinn&lt;/a&gt; is a popular QUIC implementation in Rust. I reported two denial-of-service bugs to the maintainers, both are fixed. One lives in the UDP layer below QUIC, the other in how quinn handles a peer's transport parameters.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. recv_single() spins after a truncated datagram
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/quinn-rs/quinn/security/advisories/GHSA-6pp4-4cxf-xf88" rel="noopener noreferrer"&gt;GHSA-6pp4-4cxf-xf88&lt;/a&gt; · High · quinn-udp, fixed in 0.5.16 and 0.6.3&lt;/p&gt;

&lt;p&gt;&lt;code&gt;recv_single&lt;/code&gt; is the Unix fallback that receives one datagram per &lt;code&gt;recvmsg&lt;/code&gt; call. It is used on OpenBSD and some other Unix and Apple configurations. The normal Linux batched path and Windows use different code.&lt;/p&gt;

&lt;p&gt;Two details combined into a loop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The code checked the &lt;code&gt;MSG_TRUNC&lt;/code&gt; flag before checking whether &lt;code&gt;recvmsg&lt;/code&gt; had failed.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;msghdr&lt;/code&gt; was initialized only once, so its flags survived from one call to the next.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So after a datagram that was too big for the buffer, the next call returned &lt;code&gt;EAGAIN&lt;/code&gt;, but the old &lt;code&gt;MSG_TRUNC&lt;/code&gt; flag was still set. The code treated that as "another truncated datagram, discard and try again" instead of returning &lt;code&gt;WouldBlock&lt;/code&gt;. The endpoint's runtime thread kept calling &lt;code&gt;recvmsg&lt;/code&gt; until a normal datagram happened to arrive. All of this happens before any QUIC packet authentication.&lt;/p&gt;

&lt;p&gt;The fix I proposed returns errors before looking at the flags and re-initializes the header before every syscall.&lt;/p&gt;

&lt;p&gt;I reproduced it with a localhost regression test that calls &lt;code&gt;recv_single&lt;/code&gt; directly with a 16-byte datagram and an 8-byte buffer. The test fails before the fix and passes after it. I checked the OpenBSD kernel source for the relevant semantics but did not run it on native OpenBSD.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; check the return value before you read output parameters. And if a struct is both input and output for a syscall, reset it before each call.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. A peer can make clamp() panic
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/quinn-rs/quinn/security/advisories/GHSA-3g6h-r5qx-f6hq" rel="noopener noreferrer"&gt;GHSA-3g6h-r5qx-f6hq&lt;/a&gt; / RUSTSEC-2026-0340 · High (CVSS 7.5) · quinn-proto, fixed in 0.11.18&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Duration::clamp(min, max)&lt;/code&gt; panics if &lt;code&gt;min &amp;gt; max&lt;/code&gt;. quinn uses it to compute the ACK delay when sending ACK_FREQUENCY frames, with a lower bound that comes from the peer's &lt;code&gt;min_ack_delay&lt;/code&gt; transport parameter.&lt;/p&gt;

&lt;p&gt;A peer could send &lt;code&gt;min_ack_delay = 100 ms&lt;/code&gt; together with &lt;code&gt;max_ack_delay = 100 ms&lt;/code&gt;. Both pass validation. At an RTT of 5 ms, the upper bound is &lt;code&gt;max(5 ms, 25 ms) = 25 ms&lt;/code&gt;, which is lower than the 100 ms lower bound. A minimal illustration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;time&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;lower&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_millis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// from the peer's min_ack_delay&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;upper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_millis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// max(rtt, 25 ms) at a 5 ms RTT&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_millis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.clamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// panics: min &amp;gt; max&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This only affects connections where sending ACK_FREQUENCY is enabled with &lt;code&gt;TransportConfig::ack_frequency_config(Some(...))&lt;/code&gt;, which is off by default. Whether the panic takes down the whole process depends on how the application handles panics, I did not verify a process-wide crash.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; &lt;code&gt;clamp&lt;/code&gt; has a precondition. If either bound comes from the network, check the relation between the bounds, not just each value on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Updating
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;quinn-udp: 0.5.16 or 0.6.3 and later&lt;/li&gt;
&lt;li&gt;quinn-proto: 0.11.18 and later&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks to the quinn maintainers for the fixes and the credit in the advisories.&lt;/p&gt;

</description>
      <category>security</category>
      <category>rust</category>
      <category>quic</category>
      <category>networking</category>
    </item>
    <item>
      <title>My toxic relationship with Meta's bug bounty program</title>
      <dc:creator>zerox80</dc:creator>
      <pubDate>Thu, 05 Nov 2026 19:26:27 +0000</pubDate>
      <link>https://dev.to/zerox80/my-toxic-relationship-with-metas-bug-bounty-program-3f9c</link>
      <guid>https://dev.to/zerox80/my-toxic-relationship-with-metas-bug-bounty-program-3f9c</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Update, Nov 5 (evening):&lt;/strong&gt; A few hours after I published this, Meta's bug bounty team reached me by email. They had replied before, but a bug in their platform kept those messages from reaching me. The report is confirmed and resolved and is waiting for a bounty decision. They can't help with the Facebook account, but we can now talk by email. So less ghosting than it looked like, more of a broken mailbox.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every relationship starts with good intentions. On October 9, I created my first Facebook account for one reason: to report a security bug to Meta.&lt;/p&gt;

&lt;p&gt;Within about two hours, the account was permanently disabled. I did the video selfie verification they asked for. That felt fast for a relationship that was two hours old. My appeal was rejected anyway.&lt;/p&gt;

&lt;p&gt;Here is the catch: Meta's bug bounty portal sends you through Facebook to open a report. I can see that my report exists. I just can't open it, answer questions or add evidence.&lt;/p&gt;

&lt;p&gt;I don't think anybody did this on purpose. To me it looks like an automated false positive: brand new account, no history, and the only thing it ever did was file a security report.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it went
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Oct 9:&lt;/strong&gt; I submit the report and lose the account. I email security@ and &lt;a href="mailto:bugbounty@"&gt;bugbounty@&lt;/a&gt;. No reply reaches me.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Oct 13:&lt;/strong&gt; I open a &lt;a href="https://github.com/facebook/mvfst/issues/470" rel="noopener noreferrer"&gt;public GitHub issue&lt;/a&gt;, without technical details, asking how to reach the team.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Oct 24:&lt;/strong&gt; Day 15. I ping the maintainers. One of them answers the same day and asks me to email the details. The report now shows as "Triaged".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Oct 25:&lt;/strong&gt; &lt;a href="https://github.com/facebook/mvfst/commit/22373fcd3fc517e6d4fe753c557ca0655d3c4200" rel="noopener noreferrer"&gt;The fix lands in mvfst.&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Oct 26:&lt;/strong&gt; A Meta engineer confirms by email that the fix came from my report and says the bug bounty team will reach out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nov 3:&lt;/strong&gt; CERT-Bund, Germany's national CERT, forwards my request to Meta. They take no position on the account or the bounty. So yes, we have reached the stage where a federal agency passes notes between us.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nov 5:&lt;/strong&gt; A few hours after this post went up, the bug bounty team reaches me by email. Their earlier replies had been lost to a delivery bug on their side.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To be fair, the people who write the code were great. Two engineers on the mvfst team answered a stranger's ping and took it seriously. The silence turned out to be a process problem, not a people problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug
&lt;/h2&gt;

&lt;p&gt;mvfst is Meta's open source QUIC implementation. QUIC has a packet called Retry, which arrives before the handshake and carries a 16-byte integrity tag. &lt;a href="https://www.rfc-editor.org/rfc/rfc9001#section-5.8" rel="noopener noreferrer"&gt;RFC 9001&lt;/a&gt; says what the tag is for: only someone who has seen the client's Initial packet should be able to send a valid Retry.&lt;/p&gt;

&lt;p&gt;mvfst computed that tag correctly. Then it ignored the answer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// verifyRetryIntegrityTag returns Expected&amp;lt;bool, QuicError&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;clientConn_&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;clientHandshakeLayer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;verifyRetryIntegrityTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;originalDstConnId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;retryPacket&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// drop the packet&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With a bad tag, the function returns a successful &lt;code&gt;Expected&lt;/code&gt; that contains &lt;code&gt;false&lt;/code&gt;. But &lt;code&gt;!expected&lt;/code&gt; asks "is there an error?", not "is the value false?". So the drop branch never ran for a bad tag.&lt;/p&gt;

&lt;p&gt;The existing unit test made the same mistake. &lt;code&gt;EXPECT_TRUE(verifyRetryIntegrityTag(...))&lt;/code&gt; passes for both &lt;code&gt;true&lt;/code&gt; and &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The result: the client accepted a Retry with any tag, took the attacker's connection ID and token, reset its state and restarted the handshake.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually means
&lt;/h2&gt;

&lt;p&gt;The attacker here is blind and off-path. They have to spoof the server's address, know the client's IP and UDP port, and land one datagram in a short window during connection setup. That is a real hurdle. An attacker who is on the path gains nothing from this bug, because they can compute a valid tag anyway.&lt;/p&gt;

&lt;p&gt;I tested it with mvfst's own echo client and server over real UDP on my own machine, using a Retry with an all-zero tag:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;QUIC v1:&lt;/strong&gt; the connection attempt fails with &lt;code&gt;TRANSPORT_PARAMETER_ERROR&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;mvfst's own version (&lt;code&gt;0xfaceb002&lt;/code&gt;, the client default):&lt;/strong&gt; the handshake completes with the attacker's connection ID and token, and no error is raised.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;With the fix:&lt;/strong&gt; the Retry is dropped and the connection completes normally.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I did not do: send this over the internet, test any Meta system, or check current Facebook or Instagram app builds. The attacker does not get the handshake or application keys. My own rating is Medium (CVSS 6.5). Meta's rating? See above, I can't open the report.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;auto&lt;/span&gt; &lt;span class="n"&gt;integrityResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;clientConn_&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;clientHandshakeLayer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;verifyRetryIntegrityTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;originalDstConnId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;retryPacket&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;integrityResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;has_value&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!*&lt;/span&gt;&lt;span class="n"&gt;integrityResult&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// drop the packet&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you embed the mvfst client, use &lt;code&gt;v2026.10.26.00&lt;/code&gt; or later.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took away
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Expected&amp;lt;bool, E&amp;gt;&lt;/code&gt; has two kinds of "no".&lt;/strong&gt; If you only need yes or no, return &lt;code&gt;bool&lt;/code&gt;. If you only need success or error, return &lt;code&gt;Expected&amp;lt;void, E&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A test that never sees bad input doesn't test validation.&lt;/strong&gt; The bug survived because nothing ever fed the check a wrong tag and looked at the result.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A social media account should not be the only door to a security report.&lt;/strong&gt; When automation closes that door, researchers need another way in.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where we stand
&lt;/h2&gt;

&lt;p&gt;The bug is fixed, and that matters most. The report is confirmed and resolved, and the bounty decision is pending. Thanks to the mvfst engineers, to CERT-Bund, and to the bug bounty team for following up.&lt;/p&gt;

&lt;p&gt;The Facebook account is still gone, and the security team can't help with that. If you're new to Meta's program, expect a brand-new Facebook account to get flagged, and keep a second channel like email or a coordinator open.&lt;/p&gt;

&lt;p&gt;Meta, it was partly your mailbox after all. Your account integrity system and I still need to talk, though.&lt;/p&gt;

</description>
      <category>security</category>
      <category>cpp</category>
      <category>quic</category>
      <category>bugbounty</category>
    </item>
  </channel>
</rss>
