From a6f7722b32bd9e0c1b86a978adcb5db270a23d59 Mon Sep 17 00:00:00 2001 From: Ryan Liptak Date: Fri, 3 Jul 2026 21:30:31 -0700 Subject: [PATCH 1/2] HostName: Include trailing . in max length calculation The 255 max length restriction is based on the representation in a packet, where each label has a byte prefixed denoting the length and the trailing dot is non-optional but specified as a zero-length label (so one byte with value 0 at the end). This means that for the purposes of validation, the trailing dot should be included in the length. This also ensures that max_len is always enough to hold a validated HostName, whereas before any code relying on that assumption was technically invalid as it was possible to pass through `validate` with a 256-length slice. --- lib/std/Io/net/HostName.zig | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/std/Io/net/HostName.zig b/lib/std/Io/net/HostName.zig index 26c5fc855f66a0abf270954f8947690334b5a0d7..262a86386b60f9b3b4585e7749089c02eca47efc 100644 --- a/lib/std/Io/net/HostName.zig +++ b/lib/std/Io/net/HostName.zig @@ -28,11 +28,11 @@ pub const ValidateError = error{ pub fn validate(bytes: []const u8) ValidateError!void { if (bytes.len == 0) return error.InvalidHostName; - // Ignore trailing dot (FQDN). It doesn't count toward our length. - const end = if (bytes[bytes.len - 1] == '.') bytes.len - 1 else bytes.len; - // The accepted maximum length of a hostname, including labels and dots. - if (end > max_len) return error.NameTooLong; + if (bytes.len > max_len) return error.NameTooLong; + + // Ignore trailing dot (FQDN). + const end = if (bytes[bytes.len - 1] == '.') bytes.len - 1 else bytes.len; // Hostnames are divided into dot-separated "labels", which: // @@ -83,7 +83,6 @@ test validate { const many_a_dot_buf: [127][2]u8 = @splat(.{ 'a', '.' }); const many_a_dot: []const u8 = @ptrCast(&many_a_dot_buf); try validate(many_a_dot ++ "a"); // Total length 255 (valid) - try validate(many_a_dot ++ "a."); // Total length 255 + trailing dot (valid) // Invalid hostnames try std.testing.expectError(error.InvalidHostName, validate("")); @@ -98,8 +97,8 @@ test validate { try std.testing.expectError(error.InvalidHostName, validate(".")); try std.testing.expectError(error.InvalidHostName, validate("..")); try std.testing.expectError(error.InvalidHostName, validate(&many_a ++ "a.com")); // Label length 64 (too long) + try std.testing.expectError(error.NameTooLong, validate(many_a_dot ++ "a.")); // Total length 255 + trailing dot (too long) try std.testing.expectError(error.NameTooLong, validate(many_a_dot ++ "ab")); // Total length 256 (too long) - try std.testing.expectError(error.NameTooLong, validate(many_a_dot ++ "ab.")); // Total length 256 + trailing dot (too long) } pub fn init(bytes: []const u8) ValidateError!HostName { -- 2.54.0 From b529a94e14725ea5f56ab4f48de0801c9f7e0b8b Mon Sep 17 00:00:00 2001 From: Ryan Liptak Date: Fri, 3 Jul 2026 22:10:03 -0700 Subject: [PATCH 2/2] Uri: Decouple RFC1123 HostName validation from Uri Before this commit, Uri was sometimes using HostName.validate for `host` (in resolveInPlace) and sometimes not (in parseAfterScheme). On its own, this was a problem, but the bigger problem is that RFC3986 (Uri) has a much different idea of what a valid host name is than RFC1123 (HostName), and so just making Uri consistently use HostName.validate would make it less useful overall. Instead, all `HostName`-related stuff has been removed from `Uri`. `Uri.getHost` has been moved to `HostName.fromUri` (without a graceful deprecation, since the semantics are different enough for users to need to evaluate usage sites), while `Uri.getHostAlloc` has been removed entirely. --- lib/std/Io/net/HostName.zig | 12 ++++++++++ lib/std/Uri.zig | 47 ++++--------------------------------- lib/std/http/Client.zig | 17 ++++++++++---- 3 files changed, 30 insertions(+), 46 deletions(-) diff --git a/lib/std/Io/net/HostName.zig b/lib/std/Io/net/HostName.zig index 262a86386b60f9b3b4585e7749089c02eca47efc..6249e47dfcda57c603d42042094abbde235a62b1 100644 --- a/lib/std/Io/net/HostName.zig +++ b/lib/std/Io/net/HostName.zig @@ -19,6 +19,18 @@ bytes: []const u8, pub const max_len = 255; +pub const FromUriError = error{UriMissingHost} || ValidateError; + +/// Returned `HostName.bytes` may point into `buffer` or `uri.host`. +pub fn fromUri(uri: std.Uri, buffer: *[HostName.max_len]u8) FromUriError!HostName { + const component = uri.host orelse return error.UriMissingHost; + const bytes = component.toRaw(buffer) catch |err| switch (err) { + error.NoSpaceLeft => return error.NameTooLong, + }; + try validate(bytes); + return .{ .bytes = bytes }; +} + pub const ValidateError = error{ NameTooLong, InvalidHostName, diff --git a/lib/std/Uri.zig b/lib/std/Uri.zig index 352af5aed73d4bd11ed7302b44669d52229d9f9d..dccda6e89a3fab38b2bb096da54aae24dcccb018 100644 --- a/lib/std/Uri.zig +++ b/lib/std/Uri.zig @@ -7,43 +7,18 @@ const testing = std.testing; const Uri = @This(); const Allocator = std.mem.Allocator; const Writer = std.Io.Writer; -const HostName = std.Io.net.HostName; scheme: []const u8, user: ?Component = null, password: ?Component = null, -/// If non-null, already validated. host: ?Component = null, port: ?u16 = null, path: Component = Component.empty, query: ?Component = null, fragment: ?Component = null, -pub const GetHostError = error{UriMissingHost}; - -/// Returned value may point into `buffer` or be the original string. -/// -/// See also: -/// * `getHostAlloc` -pub fn getHost(uri: Uri, buffer: *[HostName.max_len]u8) GetHostError!HostName { - const component = uri.host orelse return error.UriMissingHost; - const bytes = component.toRaw(buffer) catch |err| switch (err) { - error.NoSpaceLeft => unreachable, // `host` already validated. - }; - return .{ .bytes = bytes }; -} - -pub const GetHostAllocError = GetHostError || error{OutOfMemory}; - -/// Returned value may point into `buffer` or be the original string. -/// -/// See also: -/// * `getHost` -pub fn getHostAlloc(uri: Uri, arena: Allocator) GetHostAllocError!HostName { - const component = uri.host orelse return error.UriMissingHost; - const bytes = try component.toRawMaybeAlloc(arena); - return .{ .bytes = bytes }; -} +pub const getHost = @compileError("This function has been moved to std.Io.net.HostName.fromUri"); +pub const getHostAlloc = @compileError("This function has been deleted. See std.Io.net.HostName.fromUri instead"); pub const Component = union(enum) { /// Invalid characters in this component must be percent encoded @@ -403,7 +378,7 @@ pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceE .scheme = new_parsed.scheme, .user = new_parsed.user, .password = new_parsed.password, - .host = try validateHostComponent(new_parsed.host), + .host = new_parsed.host, .port = new_parsed.port, .path = remove_dot_segments(new_path), .query = new_parsed.query, @@ -414,7 +389,7 @@ pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceE .scheme = base.scheme, .user = new_parsed.user, .password = new_parsed.password, - .host = try validateHostComponent(host), + .host = host, .port = new_parsed.port, .path = remove_dot_segments(new_path), .query = new_parsed.query, @@ -436,7 +411,7 @@ pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceE .scheme = base.scheme, .user = base.user, .password = base.password, - .host = try validateHostComponent(base.host), + .host = base.host, .port = base.port, .path = path, .query = query, @@ -444,18 +419,6 @@ pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceE }; } -fn validateHostComponent(optional_component: ?Component) error{InvalidHostName}!?Component { - const component = optional_component orelse return null; - switch (component) { - .raw => |raw| HostName.validate(raw) catch return error.InvalidHostName, - .percent_encoded => |encoded| { - // TODO validate decoded name instead - HostName.validate(encoded) catch return error.InvalidHostName; - }, - } - return component; -} - /// In-place implementation of RFC 3986, Section 5.2.4. fn remove_dot_segments(path: []u8) Component { var in_i: usize = 0; diff --git a/lib/std/http/Client.zig b/lib/std/http/Client.zig index 1dd8bb657944484ae0608002395166cea3a0decf..5eca8d78a8f582c773589d9f01f4ff85027e7500 100644 --- a/lib/std/http/Client.zig +++ b/lib/std/http/Client.zig @@ -1229,7 +1229,11 @@ pub const Request = struct { const old_connection = r.connection.?; const old_host = old_connection.host(); var new_host_name_buffer: [HostName.max_len]u8 = undefined; - const new_host = try new_uri.getHost(&new_host_name_buffer); + const new_host = HostName.fromUri(new_uri, &new_host_name_buffer) catch |err| switch (err) { + error.UriMissingHost => return error.HttpRedirectLocationInvalid, + error.InvalidHostName => return error.HttpRedirectLocationInvalid, + error.NameTooLong => return error.HttpRedirectLocationOversize, + }; const keep_privileged_headers = std.ascii.eqlIgnoreCase(r.uri.scheme, new_uri.scheme) and old_host.sameParentDomain(new_host); @@ -1349,7 +1353,8 @@ fn createProxyFromEnvVar( const uri = Uri.parse(content) catch try Uri.parseAfterScheme("http", content); const protocol = Protocol.fromUri(uri) orelse return null; - const raw_host = try uri.getHostAlloc(arena); + var host_buf: [HostName.max_len]u8 = undefined; + const raw_host = try HostName.fromUri(uri, &host_buf); const authorization: ?[]const u8 = if (uri.user != null or uri.password != null) a: { const authorization = try arena.alloc(u8, basic_authorization.valueLengthFromUri(uri)); @@ -1360,7 +1365,7 @@ fn createProxyFromEnvVar( const proxy = try arena.create(Proxy); proxy.* = .{ .protocol = protocol, - .host = raw_host, + .host = .{ .bytes = try arena.dupe(u8, raw_host.bytes) }, .authorization = authorization, .port = uriPort(uri, protocol), .supports_connect = true, @@ -1624,6 +1629,7 @@ pub fn connect( pub const RequestError = ConnectTcpError || error{ UnsupportedUriScheme, UriMissingHost, + InvalidHostName, CertificateBundleLoadFailure, }; @@ -1721,7 +1727,10 @@ pub fn request( const connection = options.connection orelse c: { var host_name_buffer: [HostName.max_len]u8 = undefined; - const host_name = try uri.getHost(&host_name_buffer); + const host_name = HostName.fromUri(uri, &host_name_buffer) catch |err| switch (err) { + error.UriMissingHost => |e| return e, + error.NameTooLong, error.InvalidHostName => return error.InvalidHostName, + }; break :c try client.connect(host_name, uriPort(uri, protocol), protocol); }; -- 2.54.0