← Back to PullLight
Case Study

TOCTOU race condition in Waitress HTTP pipelining

CVSS 9.1 Critical
Repository Pylons/waitress

GHSA-9298-4cf8-g4wj — Waitress versions >=2.0.0 and <3.0.1 contain a Time-Of-Check-Time-Of-Use (TOCTOU) race condition in the HTTP pipelining path.

When channel_request_lookahead is enabled (common in deployments using the default configuration), the main thread can read a second pipelined HTTP request while the service thread is simultaneously closing the connection due to an error on the first request. The second request then gets processed on a connection that should have been dropped — leading to HTTP request smuggling and unauthorized request processing.


# src/waitress/channel.py — received() method def received(self, data): with self.requests_lock: while data: # ← NO check for will_close or close_when_flushed if self.request is None: self.request = self.parser_class(self.adj) n = self.request.received(data) # processes data even if closing if not n: break
# src/waitress/channel.py — received() method def received(self, data): with self.requests_lock: # Don't bother processing anymore data if this connection is about to close if self.will_close or self.close_when_flushed: return False while data: if self.request is None: self.request = self.parser_class(self.adj) n = self.request.received(data) if not n: break

🔴 PullLight — Critical Finding
Race condition: no close-state guard before processing pipelined data. In received(), the while-data loop starts without checking will_close or close_when_flushed. When channel_request_lookahead is enabled (default in many deployments), the main thread can read a second pipelined request while the service thread sets close_when_flushed due to an error on the first request — causing the second request to be serviced on a connection that should have been dropped. This is a TOCTOU (Time-Of-Check-Time-Of-Use) race condition (CWE-367) that can lead to HTTP request smuggling and unauthorized request processing.
→ Add a guard immediately after acquiring requests_lock:
if self.will_close or self.close_when_flushed: return False

Static analysis tools flag this as "style" not "severity"

  • CodeRabbit / Copilot: Surface-level diff review — no multi-threaded state modeling. The pipelining loop looks fine in isolation.
  • Greptile: Similar — reviews code as written, not as executed under concurrent load. The race window is invisible in a linear diff.
  • Graphite / Qodo: Focus on readability and formatting. A missing return False guard is not a style violation.

What makes this hard to catch manually

  • The bug only manifests under concurrent pipelining — requires threading awareness, not just code reading.
  • The race window is a few microseconds. It won't show up in unit tests.
  • The "fix" looks trivial — just add one guard — but the severity only becomes clear when you model the multi-threaded execution path.

AI-assisted review is only as good as the model's ability to reason about concurrent state. Most AI tools treat a diff as a sequence of line changes — a linear view of a concurrent problem. The Waitress race condition is invisible without modeling two threads accessing shared state simultaneously.

As teams push more review volume through AI agents, the risk is that agents rubber-stamp patches that look syntactically correct but miss concurrency, state machine, and TOCTOU-class bugs. PullLight's analysis considers execution context — not just what changed, but what could still go wrong when the new code runs alongside the rest of the system.

CVE-2024-49768 is a CVSS 9.1. That's the severity level reserved for remote code execution and complete system compromise. An HTTP pipelining race condition isn't glamorous — it's a subtle, low-visibility bug that a competent reviewer might miss. That's exactly the kind of finding that proves PullLight is doing something fundamentally different from a diff summarizer.