You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit b4da6f0
Browse filesBrowse the repository at this point in the historyBrowse files
fix(harness): keep decoding inside the read budget, and free what a failed start held
Round 3 security review of #20 at 0075138:
- M1: a request keeps its reader until its body has been decoded and
checked against the input schema, and calls are admitted before they are
decoded and decoded one at a time. 600 sockets each sending 1 MiB of empty
objects took the process to 5.9 GiB; they now peak at 333 MiB, the read
budget plus one document. A payload the schema refuses is dropped before
the next is decoded.
- M2: a handler thread that fails to start releases the slot held for it,
so later calls are not refused and shutdown does not wait on it.
- N1: the failure code is checked again where the header is set.
- N2: the harness exits with its code directly, so parked or writing
connection threads cannot turn shutdown into an abort.
- N3: a request beginning on a connection closed to make room is refused
unread rather than run with nowhere to answer.
- N4: a body over the whole read budget is refused 413, whatever the
declared input bound.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: docs/protocol.md
+21-4Lines changed: 21 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -67,8 +67,10 @@ own: none is a value a placement resolved.
67
67
| reading one request, head and body together (`REQUEST_READ_SECONDS`) | 10 s wall clock, from its first byte |`408` once the body has begun; a head that has not arrived is dropped unanswered |
68
68
| the request line and headers (`MAX_HEAD_BYTES`) | 32 KiB |`431`|
69
69
| requests being read at once (`READ_MEMORY_BYTES` ÷ (32 KiB + `max-input-bytes`), at most 1,024) | 256 MiB of heads and bodies: 248 readers at the default 1 MiB input |`503`, before any of the request is read |
70
+
| one request's body, whatever the declaration (`READ_MEMORY_BYTES` − 32 KiB) | just under 256 MiB |`413`: a declared `max-input-bytes` larger than the whole read budget is held to the budget |
71
+
| documents being decoded | one at a time | waits its turn, still holding its reader |
70
72
| calls whose handler is running (the declared `concurrency`) |`HARNESS_CONCURRENCY`, 32, when the declaration states 0 |`503`|
71
-
| open connections (readers + concurrency + `IDLE_CONNECTIONS`, 64) | — | the connection idle the longest is closed to make room; a new one is never refused |
73
+
| open connections (readers + concurrency + `IDLE_CONNECTIONS`, 64) | — | the connection idle the longest is closed to make room; a new one is never refused, and a request that begins on a connection in the instant it is closed is refused unread rather than run with nowhere to answer|
72
74
| an idle kept-alive connection (`IDLE_SECONDS`) | 120 s, advertised as `Keep-Alive: timeout=120`| closed |
73
75
| one write of an answer (`WRITE_STALL_SECONDS`) | 10 s | the connection is dropped |
74
76
@@ -81,6 +83,17 @@ call. A slot is held until the handler really returns, not until the call is
81
83
answered: a call answered `504` at its deadline keeps its slot while its handler
82
84
runs on, so the declared concurrency bounds author code running.
83
85
86
+
A request keeps its reader until its body has been decoded and checked against
87
+
the input schema: a decoded document can be many times the bytes it came from —
88
+
an array of empty objects is about twenty — so the readers' bound is a bound on
89
+
decoding only while decoding sits inside it. The order is: read the body, take a
90
+
concurrency slot (or answer `503` without decoding anything), decode and check
91
+
the payload one call at a time, release the reader, run the handler. A payload
92
+
the schema refuses is dropped before the next is decoded, so beyond the read
93
+
budget the process holds one document being decoded and, under each running
94
+
call's slot, a payload its contract admits — the envelope the declaration's
95
+
`max-input-bytes` and `concurrency` state.
96
+
84
97
**Concurrency is not yet one number.** Core's declaration says `0` means the
85
98
facility default and defines none, so the harness admits 32 when the declaration
86
99
is silent. The durable-work runtime decides how many calls it dispatches from
@@ -143,7 +156,9 @@ The code becomes a response header, so it must match
143
156
`^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,63}$` — the grammar the durable-work runtime reads
144
157
it with, which is narrower than core's 1 to 128 characters. A code outside it
145
158
raises `ValueError` where it is constructed, and the handler's call then answers
146
-
as any untyped error does: unproven.
159
+
as any untyped error does: unproven. The code is read-only, and checked again
160
+
where the header is set: a code changed after construction answers `500` with
161
+
no failure code.
147
162
148
163
Everything else leaves the effect **unproven**, because the harness cannot know
149
164
which side of its effect a handler stopped on. A caller that read a bare status
@@ -156,7 +171,7 @@ as proof would stop looking for a receipt that exists.
0 commit comments