1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
//! Owned accumulator for an in-progress HTTP/2 HEADERS+CONTINUATION field
//! block (RFC 9113 §4.3, §6.2, §6.10) for [`super::h2::ConnectionH2`].
//!
//! Before this module existed, `ConnectionH2::zero` played two incompatible
//! roles at once: the read-side accumulator a field block was reassembled
//! into across however many `readable()` passes a HEADERS+CONTINUATION
//! sequence took (`headers.header_block_fragment` — a `(start, len)`
//! [`kawa::repr::Slice`] window over `zero.storage`, extended in place), and
//! the write-side scratch buffer every control-frame flush (WINDOW_UPDATE,
//! RST_STREAM, GOAWAY) cleared and reused. LIFECYCLE.md invariant 24 has the
//! full account; four bugs shipped from the two roles sharing one buffer —
//! sozu-proxy/sozu#1396, #1397, #1401 and #1423 — the first three each
//! patched with their own `header_block_reassembly_in_progress()` guard at
//! their own clobber site, and #1423 found a fourth clobber site with no
//! guard at all.
//!
//! [`HeaderBlockAccumulator`] gives the read side a real owned lifetime
//! (`Vec<u8>`) instead of borrowing a window into `zero.storage`. A HEADERS
//! frame without END_HEADERS seeds it via [`HeaderBlockAccumulator::begin`]
//! with a copy of its own fragment (the same "copy out before the buffer
//! changes purpose" move the CVE-2024-27316 abort path in `h2.rs` already
//! made defensively, generalised to the happy path); every CONTINUATION
//! frame's payload is folded in via [`HeaderBlockAccumulator::append`] as
//! soon as it finishes being read, with `zero.storage` cleared by the caller
//! in the same step; the block is retired via [`HeaderBlockAccumulator::finish`]
//! — decoded on a completed block, or handed to
//! `DiscardedFieldBlock::Continuation` unread on a CVE-2024-27316 refusal —
//! either way returning ownership of the bytes and resetting to idle. Every
//! early-return path in `ConnectionH2::handle_headers_frame` that could
//! abandon an in-progress reassembly (e.g. an RFC 7540 §5.3.1 PRIORITY
//! self-dependency reset) must retire it before returning, or the
//! `is_in_progress()` flag leaks into the NEXT HEADERS frame processed on
//! this connection — which may belong to an entirely different stream —
//! and that frame's own "read from `zero.storage`" fast path is silently
//! skipped in favour of the leaked stream's stale bytes. This is not a
//! hypothetical: it shipped once, in this module's first changeset, and was
//! caught only by review (`a_priority_self_dependency_reset_does_not_leak_the_reassembly_accumulator`,
//! `h2.rs`) rather than by any assertion, because the accumulator's own
//! `begin()`/`append()`/`finish()` debug_asserts only fire on a call that
//! actually happens — a call that is *silently skipped* (the `if
//! !self.header_reassembly.is_in_progress() { begin(...) }` gate in the
//! `!end_headers` branch) asserts nothing at all.
//!
//! No write-side code holds a field named `header_reassembly` — that is
//! true in every build profile, and it is what makes a control-frame flush
//! unable to reach these bytes: not that misuse is impossible, but that no
//! write-side site currently NAMES the field to misuse. `begin`/`append`/
//! `finish`'s `debug_assert!`s only run in debug/test/e2e/fuzz builds; a
//! release build has none of them, so a second `begin()` while already
//! `in_progress` silently `clear()`s the accumulated history instead of
//! panicking — the exact #1396/#1397 failure mode, just relocated to this
//! type instead of `zero.storage` — and `append()`/`finish()` called while
//! idle silently grow or return meaningless bytes. B1 above is exactly
//! that: a same-module, `pub(super)` misuse the type could not have
//! prevented on its own, caught only because a human reviewed the code
//! that calls these methods. That is what closes the #1396/#1397/#1401
//! class for the write side, and is the majority of what closes #1423 —
//! but not the literal, narrow complaint #1423 was filed against, which is
//! answered below.
//!
//! **Eliminated since #1604 as well**: control frames are serialised into the
//! connection's separate ordered output queue, so no write-side site clears
//! or reuses `zero.storage` any more, and the six guards this paragraph used
//! to list (the hung-up, WINDOW_UPDATE and RST_STREAM stages, the
//! deferred-initial-GOAWAY check, `graceful_goaway`'s decision and the
//! shutdown flush) are all gone — the last, `graceful_goaway`'s defer-or-send
//! drain policy, together with `header_block_reassembly_in_progress()` itself
//! in #1637. The paragraph below is the account they were written against.
//!
//! **What was *not* eliminated by this module, and needed guarding until
//! #1604** — checked via `header_block_reassembly_in_progress()`: a single
//! CONTINUATION frame's payload can still be *mid-flight* in `zero.storage`
//! — a partial `socket_read()` that has not yet finished this one frame —
//! when a write pass runs in the same event-loop sweep. That window is real
//! (TCP segmentation can split any frame's payload across reads) and losing
//! those bytes there would still corrupt an otherwise-completable block.
//!
//! #1423 was filed against exactly one of those sites having no guard: the
//! frontend-hung-up-while-draining stage. The step that introduced this
//! module shipped that stage genuinely unguarded, reasoning that
//! `Ready::HUP` means "no further bytes can ever arrive" — that reasoning
//! is wrong. `Ready::HUP` is `is_read_closed() || is_write_closed()`
//! (`command/src/ready.rs`), and mio documents `is_read_closed()` as true
//! not only on a full close but also on a TCP half-close: a FIN with data
//! the peer already sent still sitting, unread, in the kernel receive
//! queue. `drive_frontend_shutdown_io` (`mod.rs`) force-calls `readable()`
//! for H2 on every `shutting_down()` poll, so a CONTINUATION frame split
//! across TCP segments landing a HUP event alongside its first segment is
//! the ordinary soft-stop path, not a contrived corner case — proven by
//! `a_continuation_frame_split_by_tcp_segmentation_survives_a_hup_while_draining`
//! (`h2.rs`), red on the unguarded stage with the exact
//! `StringDecodingError(NotEnoughOctets)` / decoder-desync failure shape
//! #1397/#1401 already showed. The frontend-hung-up-while-draining stage
//! now shares the same guard its three siblings already had, closing
//! #1423 for real.
//!
//! No dependency was added: this is a plain `Vec<u8>`, exactly what
//! `CONTRIBUTING.md`'s dependency policy and this step's task both require.
/// Owned accumulator for a HEADERS+CONTINUATION field block reassembly in
/// progress. `in_progress` is tracked independently of `bytes.is_empty()` so
/// a legitimately empty first fragment (e.g. a HEADERS frame carrying only
/// PRIORITY, no field-block bytes yet, with more CONTINUATION frames still
/// to come) cannot be mistaken for "no reassembly running" — see
/// `ConnectionH2::handle_headers_frame`'s buffer-selection branch, the one
/// caller that reads it.
pub