Gal Bar Nahum's Blog

MadeYouReset - The Problem With Request Cancellation

If you’ve been following along, we’ve already seen why HTTP/2 makes it easy to spin up work and why MadeYouReset can push servers into unbounded in‑flight requests. In this post, we’ll talk about the thing that sounds like it should save the day but usually doesn’t: end‑to‑end request cancellation.

What is End-to-end Cancellation

By “end‑to‑end cancellation” I mean: once a stream closes (client cancel or server RST_STREAM), all work for that request actually stops across layers.

It is not “Just don’t write the response” or “Return early while a DB query keeps running” - those are exactly the situations in which MadeYouReset shines.

End-to-end Cancellation is Hard

There are common problems and complications that need to be addressed when supporting end-to-end cancellation:

  1. You have to forward the “stop” signal everywhere. Miss one place and that bit keeps running.
  2. Many libraries don’t know how to stop mid‑way (DB calls, RPCs, file reads), so they just keep going.
  3. Stopping at the wrong moment can leave a mess (open handles, half‑done writes, weird state, leaked memory).

Usually, cancellation is treated as optional. Some frameworks expose a signal (like Node.js HTTP/2 aborted). During disclosure I heard variations of: “we expose it; it’s not our job to propagate it”. To be clear: that’s a product stance, which is totally fine - The signal exists, but ownership is pushed to app developers. The problem is that the feature is framed as an optional performance issue, not security. MadeYouReset shows cancellation isn’t a nice‑to‑have, it has real security implications. If frameworks present it as optional rather than security‑relevant, developers will follow suit. It’s on frameworks to fix the framing.

Adding Support For End-to-end Cancallation is Also Hard

Even if you want to do it, it’s a very heavy lift:

End-to-end Cancellation In The Wild - Hyper

In my research, the only stack I found that actually stops handler execution mid‑way when an HTTP/2 stream is reset is Rust’s Hyper.

That’s really awesome - but there still might be issues with that. We’ll see in demos that “significant impact” can still show up even when end‑to‑end cancellation exists.

If you prefer to jump straight to the demos, feel free. Otherwise, here’s a short overview of how cancellation works in Hyper and Rust. I’m not a Rust expert, so here are some solid references from people who really know Rust:

How Hyper Handles Cancellation

In Rust, a future represents an operation that will eventually resolve with a value or an error. It’s a way of handling asynchronous tasks without blocking other operations. When you call .await on a future, you are asking the system to check whether the task is finished. If it isn’t, the system will keep checking it at intervals.

In Hyper, when an HTTP/2 stream is reset (via a RST_STREAM frame), the future associated with that stream is dropped. This cancellation mechanism ensures that all ongoing operations tied to that stream are immediately stopped and cleaned up.

How does Hyper stop ongoing work?

Significant Impact Even With Built In End-to-end Cancellation

First, to confirm that Hyper (and our server) really supports mid‑execution request cancellation, run the PoC on a simple Hyper server that handles a request like this:

  1. Append “1” to /tmp/hello1
  2. Sleep for 200 milliseconds
  3. Append “2” to /tmp/hello2

If requests are being canceled mid‑execution, you should see significantly more 1s than 2s.

Now that we’ve confirmed requests are actually canceled, how do we find the significant impact we’re after?

We will see that end‑to‑end cancellation is still hard, because of the two facts we mentioned earlier:

  1. The “stop” signal needs to be forwarded everywhere.
  2. Many libraries don’t know how to stop mid‑way, so they just keep going.

Significant Impact Using Sqlx

Now we understand why the demo I showed in the previous post is so interesting. We learned that SQL has sequential behaviors that can bottleneck concurrency. No we also understand that in sqlx’s case, it also queues work that doesn’t cancel when the originating future in Rust is canceled - that’s double trouble!

Here is a video of running the PoC on a hyper server that queries a MySQL db using sqlx:

An important side note: Hyper’s HTTP/2 library (h2) was patched in june, and now includes a mitigation for MadeYouReset by not allowing many server initiated RST_STREAMs per connection. This protects h2 based servers that have upgraded. Deployments pinned to older h2 versions may still be exposed. No CVE was issued for h2 (appropriately - it’s a protocol library, not a standalone server), so you might not have seen the usual advisories. I interacted with sean - one of the maintainers of h2 and hyper - which was one of the firsts to address the issue. We also had some very fruitful discussions on the matter, I highly recommend checking his blog.

Summary

Even with built‑in request cancellation, making it truly end‑to‑end is hard. Signals often don’t propagate everywhere, and many libraries won’t fully abort mid‑flight. Cancellation is valuable, but it isn’t a complete defense against MadeYouReset. My recommendation is to mitigate MadeYouReset at the protocol level, as I described in the Technical Details post.