authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-08-15 21:52:18-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-08-16 00:16:15-07:00
loga0f9a5e78d1cf419e88c493f6f57e486df236e35
treeb91fc5370cf921491e50da075e0d3bc609da6a73
parent07b753f22ba87357dca4aec3e93fbca912ac2197

std: more reliable HTTP and TLS networking

* std.Io.Reader: fix confused semantics of rebase. Before it was ambiguous whether it was supposed to be based on end or seek. Now it is clearly based on seek, with an added assertion for clarity. * std.crypto.tls.Client: fix panic due to not enough buffer size available. Also, avoid unnecessary rebasing. * std.http.Reader: introduce max_head_len to limit HTTP header length. This prevents crash in underlying reader which may require a minimum buffer length. * std.http.Client: choose better buffer sizes for streams and TLS client. Crucially, the buffer shared by HTTP reader and TLS client needs to be big enough for all http headers *and* the max TLS record size. Bump HTTP header size default from 4K to 8K. fixes #24872 I have noticed however that there are still fetch problems

6 files changed, 49 insertions(+), 46 deletions(-)

lib/std/Io.zig+6-1
...@@ -717,7 +717,12 @@ pub fn Poller(comptime StreamEnum: type) type {...@@ -717,7 +717,12 @@ pub fn Poller(comptime StreamEnum: type) type {
717 const unused = r.buffer[r.end..];717 const unused = r.buffer[r.end..];
718 if (unused.len >= min_len) return unused;718 if (unused.len >= min_len) return unused;
719 }719 }
720 if (r.seek > 0) r.rebase(r.buffer.len) catch unreachable;720 if (r.seek > 0) {
721 const data = r.buffer[r.seek..r.end];
722 @memmove(r.buffer[0..data.len], data);
723 r.seek = 0;
724 r.end = data.len;
725 }
721 {726 {
722 var list: std.ArrayListUnmanaged(u8) = .{727 var list: std.ArrayListUnmanaged(u8) = .{
723 .items = r.buffer[0..r.end],728 .items = r.buffer[0..r.end],
lib/std/Io/Reader.zig+7-24
...@@ -86,12 +86,12 @@ pub const VTable = struct {...@@ -86,12 +86,12 @@ pub const VTable = struct {
86 /// `Reader.buffer`, whichever is bigger.86 /// `Reader.buffer`, whichever is bigger.
87 readVec: *const fn (r: *Reader, data: [][]u8) Error!usize = defaultReadVec,87 readVec: *const fn (r: *Reader, data: [][]u8) Error!usize = defaultReadVec,
8888
89 /// Ensures `capacity` more data can be buffered without rebasing.89 /// Ensures `capacity` data can be buffered without rebasing.
90 ///90 ///
91 /// Asserts `capacity` is within buffer capacity, or that the stream ends91 /// Asserts `capacity` is within buffer capacity, or that the stream ends
92 /// within `capacity` bytes.92 /// within `capacity` bytes.
93 ///93 ///
94 /// Only called when `capacity` cannot fit into the unused capacity of94 /// Only called when `capacity` cannot be satisfied by unused capacity of
95 /// `buffer`.95 /// `buffer`.
96 ///96 ///
97 /// The default implementation moves buffered data to the start of97 /// The default implementation moves buffered data to the start of
...@@ -1037,7 +1037,7 @@ fn fillUnbuffered(r: *Reader, n: usize) Error!void {...@@ -1037,7 +1037,7 @@ fn fillUnbuffered(r: *Reader, n: usize) Error!void {
1037///1037///
1038/// Asserts buffer capacity is at least 1.1038/// Asserts buffer capacity is at least 1.
1039pub fn fillMore(r: *Reader) Error!void {1039pub fn fillMore(r: *Reader) Error!void {
1040 try rebase(r, 1);1040 try rebase(r, r.end - r.seek + 1);
1041 var bufs: [1][]u8 = .{""};1041 var bufs: [1][]u8 = .{""};
1042 _ = try r.vtable.readVec(r, &bufs);1042 _ = try r.vtable.readVec(r, &bufs);
1043}1043}
...@@ -1205,24 +1205,6 @@ pub fn takeLeb128(r: *Reader, comptime Result: type) TakeLeb128Error!Result {...@@ -1205,24 +1205,6 @@ pub fn takeLeb128(r: *Reader, comptime Result: type) TakeLeb128Error!Result {
1205 } }))) orelse error.Overflow;1205 } }))) orelse error.Overflow;
1206}1206}
12071207
1208pub fn expandTotalCapacity(r: *Reader, allocator: Allocator, n: usize) Allocator.Error!void {
1209 if (n <= r.buffer.len) return;
1210 if (r.seek > 0) rebase(r, r.buffer.len);
1211 var list: ArrayList(u8) = .{
1212 .items = r.buffer[0..r.end],
1213 .capacity = r.buffer.len,
1214 };
1215 defer r.buffer = list.allocatedSlice();
1216 try list.ensureTotalCapacity(allocator, n);
1217}
1218
1219pub const FillAllocError = Error || Allocator.Error;
1220
1221pub fn fillAlloc(r: *Reader, allocator: Allocator, n: usize) FillAllocError!void {
1222 try expandTotalCapacity(r, allocator, n);
1223 return fill(r, n);
1224}
1225
1226fn takeMultipleOf7Leb128(r: *Reader, comptime Result: type) TakeLeb128Error!Result {1208fn takeMultipleOf7Leb128(r: *Reader, comptime Result: type) TakeLeb128Error!Result {
1227 const result_info = @typeInfo(Result).int;1209 const result_info = @typeInfo(Result).int;
1228 comptime assert(result_info.bits % 7 == 0);1210 comptime assert(result_info.bits % 7 == 0);
...@@ -1253,9 +1235,9 @@ fn takeMultipleOf7Leb128(r: *Reader, comptime Result: type) TakeLeb128Error!Resu...@@ -1253,9 +1235,9 @@ fn takeMultipleOf7Leb128(r: *Reader, comptime Result: type) TakeLeb128Error!Resu
1253 }1235 }
1254}1236}
12551237
1256/// Ensures `capacity` more data can be buffered without rebasing.1238/// Ensures `capacity` data can be buffered without rebasing.
1257pub fn rebase(r: *Reader, capacity: usize) RebaseError!void {1239pub fn rebase(r: *Reader, capacity: usize) RebaseError!void {
1258 if (r.end + capacity <= r.buffer.len) {1240 if (r.buffer.len - r.seek >= capacity) {
1259 @branchHint(.likely);1241 @branchHint(.likely);
1260 return;1242 return;
1261 }1243 }
...@@ -1263,11 +1245,12 @@ pub fn rebase(r: *Reader, capacity: usize) RebaseError!void {...@@ -1263,11 +1245,12 @@ pub fn rebase(r: *Reader, capacity: usize) RebaseError!void {
1263}1245}
12641246
1265pub fn defaultRebase(r: *Reader, capacity: usize) RebaseError!void {1247pub fn defaultRebase(r: *Reader, capacity: usize) RebaseError!void {
1266 if (r.end <= r.buffer.len - capacity) return;1248 assert(r.buffer.len - r.seek < capacity);
1267 const data = r.buffer[r.seek..r.end];1249 const data = r.buffer[r.seek..r.end];
1268 @memmove(r.buffer[0..data.len], data);1250 @memmove(r.buffer[0..data.len], data);
1269 r.seek = 0;1251 r.seek = 0;
1270 r.end = data.len;1252 r.end = data.len;
1253 assert(r.buffer.len - r.seek >= capacity);
1271}1254}
12721255
1273test fixed {1256test fixed {
lib/std/crypto/tls/Client.zig+14-10
...@@ -183,7 +183,6 @@ const InitError = error{...@@ -183,7 +183,6 @@ const InitError = error{
183/// `input` is asserted to have buffer capacity at least `min_buffer_len`.183/// `input` is asserted to have buffer capacity at least `min_buffer_len`.
184pub fn init(input: *Reader, output: *Writer, options: Options) InitError!Client {184pub fn init(input: *Reader, output: *Writer, options: Options) InitError!Client {
185 assert(input.buffer.len >= min_buffer_len);185 assert(input.buffer.len >= min_buffer_len);
186 assert(output.buffer.len >= min_buffer_len);
187 const host = switch (options.host) {186 const host = switch (options.host) {
188 .no_verification => "",187 .no_verification => "",
189 .explicit => |host| host,188 .explicit => |host| host,
...@@ -1124,12 +1123,6 @@ fn readIndirect(c: *Client) Reader.Error!usize {...@@ -1124,12 +1123,6 @@ fn readIndirect(c: *Client) Reader.Error!usize {
1124 if (record_end > input.buffered().len) return 0;1123 if (record_end > input.buffered().len) return 0;
1125 }1124 }
11261125
1127 if (r.seek == r.end) {
1128 r.seek = 0;
1129 r.end = 0;
1130 }
1131 const cleartext_buffer = r.buffer[r.end..];
1132
1133 const cleartext_len, const inner_ct: tls.ContentType = cleartext: switch (c.application_cipher) {1126 const cleartext_len, const inner_ct: tls.ContentType = cleartext: switch (c.application_cipher) {
1134 inline else => |*p| switch (c.tls_version) {1127 inline else => |*p| switch (c.tls_version) {
1135 .tls_1_3 => {1128 .tls_1_3 => {
...@@ -1145,7 +1138,8 @@ fn readIndirect(c: *Client) Reader.Error!usize {...@@ -1145,7 +1138,8 @@ fn readIndirect(c: *Client) Reader.Error!usize {
1145 const operand: V = pad ++ mem.toBytes(big(c.read_seq));1138 const operand: V = pad ++ mem.toBytes(big(c.read_seq));
1146 break :nonce @as(V, pv.server_iv) ^ operand;1139 break :nonce @as(V, pv.server_iv) ^ operand;
1147 };1140 };
1148 const cleartext = cleartext_buffer[0..ciphertext.len];1141 rebase(r, ciphertext.len);
1142 const cleartext = r.buffer[r.end..][0..ciphertext.len];
1149 P.AEAD.decrypt(cleartext, ciphertext, auth_tag, ad, nonce, pv.server_key) catch1143 P.AEAD.decrypt(cleartext, ciphertext, auth_tag, ad, nonce, pv.server_key) catch
1150 return failRead(c, error.TlsBadRecordMac);1144 return failRead(c, error.TlsBadRecordMac);
1151 // TODO use scalar, non-slice version1145 // TODO use scalar, non-slice version
...@@ -1171,7 +1165,8 @@ fn readIndirect(c: *Client) Reader.Error!usize {...@@ -1171,7 +1165,8 @@ fn readIndirect(c: *Client) Reader.Error!usize {
1171 };1165 };
1172 const ciphertext = input.take(message_len) catch unreachable; // already peeked1166 const ciphertext = input.take(message_len) catch unreachable; // already peeked
1173 const auth_tag = (input.takeArray(P.mac_length) catch unreachable).*; // already peeked1167 const auth_tag = (input.takeArray(P.mac_length) catch unreachable).*; // already peeked
1174 const cleartext = cleartext_buffer[0..ciphertext.len];1168 rebase(r, ciphertext.len);
1169 const cleartext = r.buffer[r.end..][0..ciphertext.len];
1175 P.AEAD.decrypt(cleartext, ciphertext, auth_tag, ad, nonce, pv.server_write_key) catch1170 P.AEAD.decrypt(cleartext, ciphertext, auth_tag, ad, nonce, pv.server_write_key) catch
1176 return failRead(c, error.TlsBadRecordMac);1171 return failRead(c, error.TlsBadRecordMac);
1177 break :cleartext .{ cleartext.len, ct };1172 break :cleartext .{ cleartext.len, ct };
...@@ -1179,7 +1174,7 @@ fn readIndirect(c: *Client) Reader.Error!usize {...@@ -1179,7 +1174,7 @@ fn readIndirect(c: *Client) Reader.Error!usize {
1179 else => unreachable,1174 else => unreachable,
1180 },1175 },
1181 };1176 };
1182 const cleartext = cleartext_buffer[0..cleartext_len];1177 const cleartext = r.buffer[r.end..][0..cleartext_len];
1183 c.read_seq = std.math.add(u64, c.read_seq, 1) catch return failRead(c, error.TlsSequenceOverflow);1178 c.read_seq = std.math.add(u64, c.read_seq, 1) catch return failRead(c, error.TlsSequenceOverflow);
1184 switch (inner_ct) {1179 switch (inner_ct) {
1185 .alert => {1180 .alert => {
...@@ -1275,6 +1270,15 @@ fn readIndirect(c: *Client) Reader.Error!usize {...@@ -1275,6 +1270,15 @@ fn readIndirect(c: *Client) Reader.Error!usize {
1275 }1270 }
1276}1271}
12771272
1273fn rebase(r: *Reader, capacity: usize) void {
1274 if (r.buffer.len - r.end >= capacity) return;
1275 const data = r.buffer[r.seek..r.end];
1276 @memmove(r.buffer[0..data.len], data);
1277 r.seek = 0;
1278 r.end = data.len;
1279 assert(r.buffer.len - r.end >= capacity);
1280}
1281
1278fn failRead(c: *Client, err: ReadError) error{ReadFailed} {1282fn failRead(c: *Client, err: ReadError) error{ReadFailed} {
1279 c.read_err = err;1283 c.read_err = err;
1280 return error.ReadFailed;1284 return error.ReadFailed;
lib/std/http.zig+3-1
...@@ -329,6 +329,7 @@ pub const Reader = struct {...@@ -329,6 +329,7 @@ pub const Reader = struct {
329 /// read from `in`.329 /// read from `in`.
330 trailers: []const u8 = &.{},330 trailers: []const u8 = &.{},
331 body_err: ?BodyError = null,331 body_err: ?BodyError = null,
332 max_head_len: usize,
332333
333 pub const RemainingChunkLen = enum(u64) {334 pub const RemainingChunkLen = enum(u64) {
334 head = 0,335 head = 0,
...@@ -387,10 +388,11 @@ pub const Reader = struct {...@@ -387,10 +388,11 @@ pub const Reader = struct {
387 pub fn receiveHead(reader: *Reader) HeadError![]const u8 {388 pub fn receiveHead(reader: *Reader) HeadError![]const u8 {
388 reader.trailers = &.{};389 reader.trailers = &.{};
389 const in = reader.in;390 const in = reader.in;
391 const max_head_len = reader.max_head_len;
390 var hp: HeadParser = .{};392 var hp: HeadParser = .{};
391 var head_len: usize = 0;393 var head_len: usize = 0;
392 while (true) {394 while (true) {
393 if (in.buffer.len - head_len == 0) return error.HttpHeadersOversize;395 if (head_len >= max_head_len) return error.HttpHeadersOversize;
394 const remaining = in.buffered()[head_len..];396 const remaining = in.buffered()[head_len..];
395 if (remaining.len == 0) {397 if (remaining.len == 0) {
396 in.fillMore() catch |err| switch (err) {398 in.fillMore() catch |err| switch (err) {
lib/std/http/Client.zig+17-10
...@@ -42,7 +42,7 @@ connection_pool: ConnectionPool = .{},...@@ -42,7 +42,7 @@ connection_pool: ConnectionPool = .{},
42///42///
43/// If the entire HTTP header cannot fit in this amount of bytes,43/// If the entire HTTP header cannot fit in this amount of bytes,
44/// `error.HttpHeadersOversize` will be returned from `Request.wait`.44/// `error.HttpHeadersOversize` will be returned from `Request.wait`.
45read_buffer_size: usize = 4096 + if (disable_tls) 0 else std.crypto.tls.Client.min_buffer_len,45read_buffer_size: usize = 8192,
46/// Each `Connection` allocates this amount for the writer buffer.46/// Each `Connection` allocates this amount for the writer buffer.
47write_buffer_size: usize = 1024,47write_buffer_size: usize = 1024,
4848
...@@ -302,18 +302,22 @@ pub const Connection = struct {...@@ -302,18 +302,22 @@ pub const Connection = struct {
302 const base = try gpa.alignedAlloc(u8, .of(Tls), alloc_len);302 const base = try gpa.alignedAlloc(u8, .of(Tls), alloc_len);
303 errdefer gpa.free(base);303 errdefer gpa.free(base);
304 const host_buffer = base[@sizeOf(Tls)..][0..remote_host.len];304 const host_buffer = base[@sizeOf(Tls)..][0..remote_host.len];
305 const tls_read_buffer = host_buffer.ptr[host_buffer.len..][0..client.tls_buffer_size];305 // The TLS client wants enough buffer for the max encrypted frame
306 // size, and the HTTP body reader wants enough buffer for the
307 // entire HTTP header. This means we need a combined upper bound.
308 const tls_read_buffer_len = client.tls_buffer_size + client.read_buffer_size;
309 const tls_read_buffer = host_buffer.ptr[host_buffer.len..][0..tls_read_buffer_len];
306 const tls_write_buffer = tls_read_buffer.ptr[tls_read_buffer.len..][0..client.tls_buffer_size];310 const tls_write_buffer = tls_read_buffer.ptr[tls_read_buffer.len..][0..client.tls_buffer_size];
307 const write_buffer = tls_write_buffer.ptr[tls_write_buffer.len..][0..client.write_buffer_size];311 const socket_write_buffer = tls_write_buffer.ptr[tls_write_buffer.len..][0..client.write_buffer_size];
308 const read_buffer = write_buffer.ptr[write_buffer.len..][0..client.read_buffer_size];312 const socket_read_buffer = socket_write_buffer.ptr[socket_write_buffer.len..][0..client.tls_buffer_size];
309 assert(base.ptr + alloc_len == read_buffer.ptr + read_buffer.len);313 assert(base.ptr + alloc_len == socket_read_buffer.ptr + socket_read_buffer.len);
310 @memcpy(host_buffer, remote_host);314 @memcpy(host_buffer, remote_host);
311 const tls: *Tls = @ptrCast(base);315 const tls: *Tls = @ptrCast(base);
312 tls.* = .{316 tls.* = .{
313 .connection = .{317 .connection = .{
314 .client = client,318 .client = client,
315 .stream_writer = stream.writer(tls_write_buffer),319 .stream_writer = stream.writer(tls_write_buffer),
316 .stream_reader = stream.reader(tls_read_buffer),320 .stream_reader = stream.reader(socket_read_buffer),
317 .pool_node = .{},321 .pool_node = .{},
318 .port = port,322 .port = port,
319 .host_len = @intCast(remote_host.len),323 .host_len = @intCast(remote_host.len),
...@@ -329,8 +333,8 @@ pub const Connection = struct {...@@ -329,8 +333,8 @@ pub const Connection = struct {
329 .host = .{ .explicit = remote_host },333 .host = .{ .explicit = remote_host },
330 .ca = .{ .bundle = client.ca_bundle },334 .ca = .{ .bundle = client.ca_bundle },
331 .ssl_key_log = client.ssl_key_log,335 .ssl_key_log = client.ssl_key_log,
332 .read_buffer = read_buffer,336 .read_buffer = tls_read_buffer,
333 .write_buffer = write_buffer,337 .write_buffer = socket_write_buffer,
334 // This is appropriate for HTTPS because the HTTP headers contain338 // This is appropriate for HTTPS because the HTTP headers contain
335 // the content length which is used to detect truncation attacks.339 // the content length which is used to detect truncation attacks.
336 .allow_truncation_attacks = true,340 .allow_truncation_attacks = true,
...@@ -348,8 +352,9 @@ pub const Connection = struct {...@@ -348,8 +352,9 @@ pub const Connection = struct {
348 }352 }
349353
350 fn allocLen(client: *Client, host_len: usize) usize {354 fn allocLen(client: *Client, host_len: usize) usize {
351 return @sizeOf(Tls) + host_len + client.tls_buffer_size + client.tls_buffer_size +355 const tls_read_buffer_len = client.tls_buffer_size + client.read_buffer_size;
352 client.write_buffer_size + client.read_buffer_size;356 return @sizeOf(Tls) + host_len + tls_read_buffer_len + client.tls_buffer_size +
357 client.write_buffer_size + client.tls_buffer_size;
353 }358 }
354359
355 fn host(tls: *Tls) []u8 {360 fn host(tls: *Tls) []u8 {
...@@ -1214,6 +1219,7 @@ pub const Request = struct {...@@ -1214,6 +1219,7 @@ pub const Request = struct {
1214 .state = .ready,1219 .state = .ready,
1215 // Populated when `http.Reader.bodyReader` is called.1220 // Populated when `http.Reader.bodyReader` is called.
1216 .interface = undefined,1221 .interface = undefined,
1222 .max_head_len = r.client.read_buffer_size,
1217 };1223 };
1218 r.redirect_behavior.subtractOne();1224 r.redirect_behavior.subtractOne();
1219 }1225 }
...@@ -1679,6 +1685,7 @@ pub fn request(...@@ -1679,6 +1685,7 @@ pub fn request(
1679 .state = .ready,1685 .state = .ready,
1680 // Populated when `http.Reader.bodyReader` is called.1686 // Populated when `http.Reader.bodyReader` is called.
1681 .interface = undefined,1687 .interface = undefined,
1688 .max_head_len = client.read_buffer_size,
1682 },1689 },
1683 .keep_alive = options.keep_alive,1690 .keep_alive = options.keep_alive,
1684 .method = method,1691 .method = method,
lib/std/http/Server.zig+2
...@@ -29,6 +29,7 @@ pub fn init(in: *Reader, out: *Writer) Server {...@@ -29,6 +29,7 @@ pub fn init(in: *Reader, out: *Writer) Server {
29 .state = .ready,29 .state = .ready,
30 // Populated when `http.Reader.bodyReader` is called.30 // Populated when `http.Reader.bodyReader` is called.
31 .interface = undefined,31 .interface = undefined,
32 .max_head_len = in.buffer.len,
32 },33 },
33 .out = out,34 .out = out,
34 };35 };
...@@ -251,6 +252,7 @@ pub const Request = struct {...@@ -251,6 +252,7 @@ pub const Request = struct {
251 .in = undefined,252 .in = undefined,
252 .state = .received_head,253 .state = .received_head,
253 .interface = undefined,254 .interface = undefined,
255 .max_head_len = 4096,
254 },256 },
255 .out = undefined,257 .out = undefined,
256 };258 };