Commit 35ce907c06

Andrew Kelley <andrew@ziglang.org>
2025-10-15 05:59:16
std.Io.net.HostName: move lookup to the interface
Unfortunately this can't be implemented "above the vtable" because various operating systems don't provide low level DNS resolution primitives such as just putting the list of nameservers in a file. Without libc on Linux it works great though! Anyway this also changes the API to be based on Io.Queue. By using a large enough buffer, reusable code can be written that does not require concurrent, yet takes advantage of responding to DNS queries as they come in. I sketched out a new implementation of `HostName.connect` to demonstrate this, but it will require an additional API (`Io.Select`) to be implemented in a future commit. This commit also introduces "uncancelable" variants for mutex locking, waiting on a condition, and putting items into a queue.
1 parent 1382e41
lib/std/Io/net/HostName.zig
@@ -63,8 +63,6 @@ pub fn eql(a: HostName, b: HostName) bool {
 
 pub const LookupOptions = struct {
     port: u16,
-    /// Must have at least length 2.
-    addresses_buffer: []IpAddress,
     canonical_name_buffer: *[max_len]u8,
     /// `null` means either.
     family: ?IpAddress.Family = null,
@@ -81,487 +79,23 @@ pub const LookupError = error{
     DetectingNetworkConfigurationFailed,
 } || Io.Clock.Error || IpAddress.BindError || Io.Cancelable;
 
-pub const LookupResult = struct {
-    /// How many `LookupOptions.addresses_buffer` elements are populated.
-    addresses_len: usize,
+pub const LookupResult = union(enum) {
+    address: IpAddress,
     canonical_name: HostName,
-
-    pub const empty: LookupResult = .{
-        .addresses_len = 0,
-        .canonical_name = undefined,
-    };
+    end: LookupError!void,
 };
 
-pub fn lookup(host_name: HostName, io: Io, options: LookupOptions) LookupError!LookupResult {
-    const name = host_name.bytes;
-    assert(name.len <= max_len);
-    assert(options.addresses_buffer.len >= 2);
-
-    if (native_os == .windows) @compileError("TODO");
-    if (builtin.link_libc) @compileError("TODO");
-    if (native_os == .linux) {
-        if (options.family != .ip6) {
-            if (IpAddress.parseIp4(name, options.port)) |addr| {
-                options.addresses_buffer[0] = addr;
-                return .{ .addresses_len = 1, .canonical_name = copyCanon(options.canonical_name_buffer, name) };
-            } else |_| {}
-        }
-        if (options.family != .ip4) {
-            if (IpAddress.parseIp6(name, options.port)) |addr| {
-                options.addresses_buffer[0] = addr;
-                return .{ .addresses_len = 1, .canonical_name = copyCanon(options.canonical_name_buffer, name) };
-            } else |_| {}
-        }
-        {
-            const result = try lookupHosts(host_name, io, options);
-            if (result.addresses_len > 0) return sortLookupResults(options, result);
-        }
-        {
-            // RFC 6761 Section 6.3.3
-            // Name resolution APIs and libraries SHOULD recognize
-            // localhost names as special and SHOULD always return the IP
-            // loopback address for address queries and negative responses
-            // for all other query types.
-
-            // Check for equal to "localhost(.)" or ends in ".localhost(.)"
-            const localhost = if (name[name.len - 1] == '.') "localhost." else "localhost";
-            if (std.mem.endsWith(u8, name, localhost) and
-                (name.len == localhost.len or name[name.len - localhost.len] == '.'))
-            {
-                var i: usize = 0;
-                if (options.family != .ip6) {
-                    options.addresses_buffer[i] = .{ .ip4 = .loopback(options.port) };
-                    i += 1;
-                }
-                if (options.family != .ip4) {
-                    options.addresses_buffer[i] = .{ .ip6 = .loopback(options.port) };
-                    i += 1;
-                }
-                const canon_name = "localhost";
-                const canon_name_dest = options.canonical_name_buffer[0..canon_name.len];
-                canon_name_dest.* = canon_name.*;
-                return sortLookupResults(options, .{
-                    .addresses_len = i,
-                    .canonical_name = .{ .bytes = canon_name_dest },
-                });
-            }
-        }
-        {
-            const result = try lookupDnsSearch(host_name, io, options);
-            if (result.addresses_len > 0) return sortLookupResults(options, result);
-        }
-        return error.UnknownHostName;
-    }
-    @compileError("unimplemented");
-}
-
-fn sortLookupResults(options: LookupOptions, result: LookupResult) !LookupResult {
-    const addresses = options.addresses_buffer[0..result.addresses_len];
-    // No further processing is needed if there are fewer than 2 results or
-    // if there are only IPv4 results.
-    if (addresses.len < 2) return result;
-    const all_ip4 = for (addresses) |a| switch (a) {
-        .ip4 => continue,
-        .ip6 => break false,
-    } else true;
-    if (all_ip4) return result;
-
-    // RFC 3484/6724 describes how destination address selection is
-    // supposed to work. However, to implement it requires making a bunch
-    // of networking syscalls, which is unnecessarily high latency,
-    // especially if implemented serially. Furthermore, rules 3, 4, and 7
-    // have excessive runtime and code size cost and dubious benefit.
-    //
-    // Therefore, this logic sorts only using values available without
-    // doing any syscalls, relying on the calling code to have a
-    // meta-strategy such as attempting connection to multiple results at
-    // once and keeping the fastest response while canceling the others.
-
-    const S = struct {
-        pub fn lessThan(s: @This(), lhs: IpAddress, rhs: IpAddress) bool {
-            return sortKey(s, lhs) < sortKey(s, rhs);
-        }
-
-        fn sortKey(s: @This(), a: IpAddress) i32 {
-            _ = s;
-            var da6: Ip6Address = .{
-                .port = 65535,
-                .bytes = undefined,
-            };
-            switch (a) {
-                .ip6 => |ip6| {
-                    da6.bytes = ip6.bytes;
-                    da6.interface = ip6.interface;
-                },
-                .ip4 => |ip4| {
-                    da6.bytes[0..12].* = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff".*;
-                    da6.bytes[12..].* = ip4.bytes;
-                },
-            }
-            const da6_scope: i32 = da6.scope();
-            const da6_prec: i32 = da6.policy().prec;
-            var key: i32 = 0;
-            key |= da6_prec << 20;
-            key |= (15 - da6_scope) << 16;
-            return key;
-        }
-    };
-    std.mem.sort(IpAddress, addresses, @as(S, .{}), S.lessThan);
-    return result;
-}
-
-fn lookupDnsSearch(host_name: HostName, io: Io, options: LookupOptions) LookupError!LookupResult {
-    const rc = ResolvConf.init(io) catch return error.ResolvConfParseFailed;
-
-    // Count dots, suppress search when >=ndots or name ends in
-    // a dot, which is an explicit request for global scope.
-    const dots = std.mem.countScalar(u8, host_name.bytes, '.');
-    const search_len = if (dots >= rc.ndots or std.mem.endsWith(u8, host_name.bytes, ".")) 0 else rc.search_len;
-    const search = rc.search_buffer[0..search_len];
-
-    var canon_name = host_name.bytes;
-
-    // Strip final dot for canon, fail if multiple trailing dots.
-    if (std.mem.endsWith(u8, canon_name, ".")) canon_name.len -= 1;
-    if (std.mem.endsWith(u8, canon_name, ".")) return error.UnknownHostName;
-
-    // Name with search domain appended is set up in `canon_name`. This
-    // both provides the desired default canonical name (if the requested
-    // name is not a CNAME record) and serves as a buffer for passing the
-    // full requested name to `lookupDns`.
-    @memcpy(options.canonical_name_buffer[0..canon_name.len], canon_name);
-    options.canonical_name_buffer[canon_name.len] = '.';
-    var it = std.mem.tokenizeAny(u8, search, " \t");
-    while (it.next()) |token| {
-        @memcpy(options.canonical_name_buffer[canon_name.len + 1 ..][0..token.len], token);
-        const lookup_canon_name = options.canonical_name_buffer[0 .. canon_name.len + 1 + token.len];
-        const result = try lookupDns(io, lookup_canon_name, &rc, options);
-        if (result.addresses_len > 0) return sortLookupResults(options, result);
-    }
-
-    const lookup_canon_name = options.canonical_name_buffer[0..canon_name.len];
-    return lookupDns(io, lookup_canon_name, &rc, options);
-}
-
-fn lookupDns(io: Io, lookup_canon_name: []const u8, rc: *const ResolvConf, options: LookupOptions) LookupError!LookupResult {
-    const family_records: [2]struct { af: IpAddress.Family, rr: u8 } = .{
-        .{ .af = .ip6, .rr = std.posix.RR.A },
-        .{ .af = .ip4, .rr = std.posix.RR.AAAA },
-    };
-    var query_buffers: [2][280]u8 = undefined;
-    var answer_buffer: [2 * 512]u8 = undefined;
-    var queries_buffer: [2][]const u8 = undefined;
-    var answers_buffer: [2][]const u8 = undefined;
-    var nq: usize = 0;
-    var answer_buffer_i: usize = 0;
-
-    for (family_records) |fr| {
-        if (options.family != fr.af) {
-            const entropy = std.crypto.random.array(u8, 2);
-            const len = writeResolutionQuery(&query_buffers[nq], 0, lookup_canon_name, 1, fr.rr, entropy);
-            queries_buffer[nq] = query_buffers[nq][0..len];
-            nq += 1;
-        }
-    }
-
-    var ip4_mapped: [ResolvConf.max_nameservers]IpAddress = undefined;
-    var any_ip6 = false;
-    for (rc.nameservers(), &ip4_mapped) |*ns, *m| {
-        m.* = .{ .ip6 = .fromAny(ns.*) };
-        any_ip6 = any_ip6 or ns.* == .ip6;
-    }
-    var socket = s: {
-        if (any_ip6) ip6: {
-            const ip6_addr: IpAddress = .{ .ip6 = .unspecified(0) };
-            const socket = ip6_addr.bind(io, .{ .ip6_only = true, .mode = .dgram }) catch |err| switch (err) {
-                error.AddressFamilyUnsupported => break :ip6,
-                else => |e| return e,
-            };
-            break :s socket;
-        }
-        any_ip6 = false;
-        const ip4_addr: IpAddress = .{ .ip4 = .unspecified(0) };
-        const socket = try ip4_addr.bind(io, .{ .mode = .dgram });
-        break :s socket;
-    };
-    defer socket.close(io);
-
-    const mapped_nameservers = if (any_ip6) ip4_mapped[0..rc.nameservers_len] else rc.nameservers();
-    const queries = queries_buffer[0..nq];
-    const answers = answers_buffer[0..queries.len];
-    var answers_remaining = answers.len;
-    for (answers) |*answer| answer.len = 0;
-
-    // boot clock is chosen because time the computer is suspended should count
-    // against time spent waiting for external messages to arrive.
-    const clock: Io.Clock = .boot;
-    var now_ts = try clock.now(io);
-    const final_ts = now_ts.addDuration(.fromSeconds(rc.timeout_seconds));
-    const attempt_duration: Io.Duration = .{
-        .nanoseconds = std.time.ns_per_s * @as(usize, rc.timeout_seconds) / rc.attempts,
-    };
-
-    send: while (now_ts.nanoseconds < final_ts.nanoseconds) : (now_ts = try clock.now(io)) {
-        const max_messages = queries_buffer.len * ResolvConf.max_nameservers;
-        {
-            var message_buffer: [max_messages]Io.net.OutgoingMessage = undefined;
-            var message_i: usize = 0;
-            for (queries, answers) |query, *answer| {
-                if (answer.len != 0) continue;
-                for (mapped_nameservers) |*ns| {
-                    message_buffer[message_i] = .{
-                        .address = ns,
-                        .data_ptr = query.ptr,
-                        .data_len = query.len,
-                    };
-                    message_i += 1;
-                }
-            }
-            _ = io.vtable.netSend(io.userdata, socket.handle, message_buffer[0..message_i], .{});
-        }
-
-        const timeout: Io.Timeout = .{ .deadline = .{
-            .raw = now_ts.addDuration(attempt_duration),
-            .clock = clock,
-        } };
-
-        while (true) {
-            var message_buffer: [max_messages]Io.net.IncomingMessage = undefined;
-            const buf = answer_buffer[answer_buffer_i..];
-            const recv_err, const recv_n = socket.receiveManyTimeout(io, &message_buffer, buf, .{}, timeout);
-            for (message_buffer[0..recv_n]) |*received_message| {
-                const reply = received_message.data;
-                // Ignore non-identifiable packets.
-                if (reply.len < 4) continue;
-
-                // Ignore replies from addresses we didn't send to.
-                const ns = for (mapped_nameservers) |*ns| {
-                    if (received_message.from.eql(ns)) break ns;
-                } else {
-                    continue;
-                };
-
-                // Find which query this answer goes with, if any.
-                const query, const answer = for (queries, answers) |query, *answer| {
-                    if (reply[0] == query[0] and reply[1] == query[1]) break .{ query, answer };
-                } else {
-                    continue;
-                };
-                if (answer.len != 0) continue;
-
-                // Only accept positive or negative responses; retry immediately on
-                // server failure, and ignore all other codes such as refusal.
-                switch (reply[3] & 15) {
-                    0, 3 => {
-                        answer.* = reply;
-                        answer_buffer_i += reply.len;
-                        answers_remaining -= 1;
-                        if (answer_buffer.len - answer_buffer_i == 0) break :send;
-                        if (answers_remaining == 0) break :send;
-                    },
-                    2 => {
-                        var retry_message: Io.net.OutgoingMessage = .{
-                            .address = ns,
-                            .data_ptr = query.ptr,
-                            .data_len = query.len,
-                        };
-                        _ = io.vtable.netSend(io.userdata, socket.handle, (&retry_message)[0..1], .{});
-                        continue;
-                    },
-                    else => continue,
-                }
-            }
-            if (recv_err) |err| switch (err) {
-                error.Canceled => return error.Canceled,
-                error.Timeout => continue :send,
-                else => continue,
-            };
-        }
-    } else {
-        return error.NameServerFailure;
-    }
-
-    var addresses_len: usize = 0;
-    var canonical_name: ?HostName = null;
-
-    for (answers) |answer| {
-        var it = DnsResponse.init(answer) catch {
-            // TODO accept a diagnostics struct and append warnings
-            continue;
-        };
-        while (it.next() catch {
-            // TODO accept a diagnostics struct and append warnings
-            continue;
-        }) |record| switch (record.rr) {
-            std.posix.RR.A => {
-                const data = record.packet[record.data_off..][0..record.data_len];
-                if (data.len != 4) return error.InvalidDnsARecord;
-                if (addresses_len < options.addresses_buffer.len) {
-                    options.addresses_buffer[addresses_len] = .{ .ip4 = .{
-                        .bytes = data[0..4].*,
-                        .port = options.port,
-                    } };
-                    addresses_len += 1;
-                }
-            },
-            std.posix.RR.AAAA => {
-                const data = record.packet[record.data_off..][0..record.data_len];
-                if (data.len != 16) return error.InvalidDnsAAAARecord;
-                if (addresses_len < options.addresses_buffer.len) {
-                    options.addresses_buffer[addresses_len] = .{ .ip6 = .{
-                        .bytes = data[0..16].*,
-                        .port = options.port,
-                    } };
-                    addresses_len += 1;
-                }
-            },
-            std.posix.RR.CNAME => {
-                _, canonical_name = expand(record.packet, record.data_off, options.canonical_name_buffer) catch
-                    return error.InvalidDnsCnameRecord;
-            },
-            else => continue,
-        };
-    }
-
-    if (addresses_len != 0) return .{
-        .addresses_len = addresses_len,
-        .canonical_name = canonical_name orelse .{ .bytes = lookup_canon_name },
-    };
-
-    return error.NameServerFailure;
-}
-
-fn lookupHosts(host_name: HostName, io: Io, options: LookupOptions) !LookupResult {
-    const file = Io.File.openAbsolute(io, "/etc/hosts", .{}) catch |err| switch (err) {
-        error.FileNotFound,
-        error.NotDir,
-        error.AccessDenied,
-        => return .empty,
-
-        error.Canceled => |e| return e,
-
-        else => {
-            // TODO populate optional diagnostic struct
-            return error.DetectingNetworkConfigurationFailed;
-        },
-    };
-    defer file.close(io);
-
-    var line_buf: [512]u8 = undefined;
-    var file_reader = file.reader(io, &line_buf);
-    return lookupHostsReader(host_name, options, &file_reader.interface) catch |err| switch (err) {
-        error.ReadFailed => switch (file_reader.err.?) {
-            error.Canceled => |e| return e,
-            else => {
-                // TODO populate optional diagnostic struct
-                return error.DetectingNetworkConfigurationFailed;
-            },
-        },
-    };
-}
-
-fn lookupHostsReader(host_name: HostName, options: LookupOptions, reader: *Io.Reader) error{ReadFailed}!LookupResult {
-    var addresses_len: usize = 0;
-    var canonical_name: ?HostName = null;
-    while (true) {
-        const line = reader.takeDelimiterExclusive('\n') catch |err| switch (err) {
-            error.StreamTooLong => {
-                // Skip lines that are too long.
-                _ = reader.discardDelimiterInclusive('\n') catch |e| switch (e) {
-                    error.EndOfStream => break,
-                    error.ReadFailed => return error.ReadFailed,
-                };
-                continue;
-            },
-            error.ReadFailed => return error.ReadFailed,
-            error.EndOfStream => break,
-        };
-        reader.toss(1);
-        var split_it = std.mem.splitScalar(u8, line, '#');
-        const no_comment_line = split_it.first();
-
-        var line_it = std.mem.tokenizeAny(u8, no_comment_line, " \t");
-        const ip_text = line_it.next() orelse continue;
-        var first_name_text: ?[]const u8 = null;
-        while (line_it.next()) |name_text| {
-            if (std.mem.eql(u8, name_text, host_name.bytes)) {
-                if (first_name_text == null) first_name_text = name_text;
-                break;
-            }
-        } else continue;
-
-        if (canonical_name == null) {
-            if (HostName.init(first_name_text.?)) |name_text| {
-                if (name_text.bytes.len <= options.canonical_name_buffer.len) {
-                    const canonical_name_dest = options.canonical_name_buffer[0..name_text.bytes.len];
-                    @memcpy(canonical_name_dest, name_text.bytes);
-                    canonical_name = .{ .bytes = canonical_name_dest };
-                }
-            } else |_| {}
-        }
-
-        if (options.family != .ip6) {
-            if (IpAddress.parseIp4(ip_text, options.port)) |addr| {
-                options.addresses_buffer[addresses_len] = addr;
-                addresses_len += 1;
-                if (options.addresses_buffer.len - addresses_len == 0) return .{
-                    .addresses_len = addresses_len,
-                    .canonical_name = canonical_name orelse copyCanon(options.canonical_name_buffer, ip_text),
-                };
-            } else |_| {}
-        }
-        if (options.family != .ip4) {
-            if (IpAddress.parseIp6(ip_text, options.port)) |addr| {
-                options.addresses_buffer[addresses_len] = addr;
-                addresses_len += 1;
-                if (options.addresses_buffer.len - addresses_len == 0) return .{
-                    .addresses_len = addresses_len,
-                    .canonical_name = canonical_name orelse copyCanon(options.canonical_name_buffer, ip_text),
-                };
-            } else |_| {}
-        }
-    }
-    if (canonical_name == null) assert(addresses_len == 0);
-    return .{
-        .addresses_len = addresses_len,
-        .canonical_name = canonical_name orelse undefined,
-    };
-}
-
-fn copyCanon(canonical_name_buffer: *[max_len]u8, name: []const u8) HostName {
-    const dest = canonical_name_buffer[0..name.len];
-    @memcpy(dest, name);
-    return .{ .bytes = dest };
-}
-
-/// Writes DNS resolution query packet data to `w`; at most 280 bytes.
-fn writeResolutionQuery(q: *[280]u8, op: u4, dname: []const u8, class: u8, ty: u8, entropy: [2]u8) usize {
-    // This implementation is ported from musl libc.
-    // A more idiomatic "ziggy" implementation would be welcome.
-    var name = dname;
-    if (std.mem.endsWith(u8, name, ".")) name.len -= 1;
-    assert(name.len <= 253);
-    const n = 17 + name.len + @intFromBool(name.len != 0);
-
-    // Construct query template - ID will be filled later
-    q[0..2].* = entropy;
-    @memset(q[2..n], 0);
-    q[2] = @as(u8, op) * 8 + 1;
-    q[5] = 1;
-    @memcpy(q[13..][0..name.len], name);
-    var i: usize = 13;
-    var j: usize = undefined;
-    while (q[i] != 0) : (i = j + 1) {
-        j = i;
-        while (q[j] != 0 and q[j] != '.') : (j += 1) {}
-        // TODO determine the circumstances for this and whether or
-        // not this should be an error.
-        if (j - i - 1 > 62) unreachable;
-        q[i - 1] = @intCast(j - i);
-    }
-    q[i + 1] = ty;
-    q[i + 3] = class;
-    return n;
+/// Adds any number of `IpAddress` into resolved, exactly one canonical_name,
+/// and then always finishes by adding one `LookupResult.end` entry.
+///
+/// Guaranteed not to block if provided queue has capacity at least 8.
+pub fn lookup(
+    host_name: HostName,
+    io: Io,
+    resolved: *Io.Queue(LookupResult),
+    options: LookupOptions,
+) void {
+    return io.vtable.netLookup(io.userdata, host_name, resolved, options);
 }
 
 pub const ExpandError = error{InvalidDnsPacket} || ValidateError;
@@ -672,33 +206,43 @@ pub fn connect(
     port: u16,
     options: IpAddress.ConnectOptions,
 ) ConnectError!Stream {
-    var addresses_buffer: [32]IpAddress = undefined;
-    var canonical_name_buffer: [HostName.max_len]u8 = undefined;
+    var canonical_name_buffer: [max_len]u8 = undefined;
+    var results_buffer: [32]HostName.LookupResult = undefined;
+    var results: Io.Queue(LookupResult) = .init(&results_buffer);
 
-    const results = try lookup(host_name, io, .{
+    var lookup_task = io.async(HostName.lookup, .{ host_name, io, &results, .{
         .port = port,
-        .addresses_buffer = &addresses_buffer,
         .canonical_name_buffer = &canonical_name_buffer,
-    });
-    const addresses = addresses_buffer[0..results.addresses_len];
-
-    if (addresses.len == 0) return error.UnknownHostName;
+    } });
+    defer lookup_task.cancel(io);
+
+    var select: Io.Select(union(enum) { ip_connect: IpAddress.ConnectError!Stream }) = .init;
+    defer select.cancel(io);
+
+    while (results.getOne(io)) |result| switch (result) {
+        .address => |address| select.async(io, .ip_connect, IpAddress.connect, .{ address, io, options }),
+        .canonical_name => continue,
+        .end => |lookup_result| {
+            try lookup_result;
+            break;
+        },
+    } else |err| return err;
 
-    // TODO instead of serially, use a Select API to send out
-    // the connections simultaneously and then keep the first
-    // successful one, canceling the rest.
+    var aggregate_error: ConnectError = error.UnknownHostName;
 
-    // TODO On Linux this should additionally use an Io.Queue based
-    // DNS resolution API in order to send out a connection after
-    // each DNS response before waiting for the rest of them.
+    while (select.remaining != 0) switch (select.wait(io)) {
+        .ip_connect => |ip_connect| if (ip_connect) |stream| return stream else |err| switch (err) {
+            error.SystemResources => |e| return e,
+            error.OptionUnsupported => |e| return e,
+            error.ProcessFdQuotaExceeded => |e| return e,
+            error.SystemFdQuotaExceeded => |e| return e,
+            error.Canceled => |e| return e,
+            error.WouldBlock => return error.Unexpected,
+            else => |e| aggregate_error = e,
+        },
+    };
 
-    for (addresses) |*addr| {
-        return addr.connect(io, options) catch |err| switch (err) {
-            error.ConnectionRefused => continue,
-            else => |e| return e,
-        };
-    }
-    return error.ConnectionRefused;
+    return aggregate_error;
 }
 
 pub const ResolvConf = struct {
@@ -713,7 +257,7 @@ pub const ResolvConf = struct {
     pub const max_nameservers = 3;
 
     /// Returns `error.StreamTooLong` if a line is longer than 512 bytes.
-    fn init(io: Io) !ResolvConf {
+    pub fn init(io: Io) !ResolvConf {
         var rc: ResolvConf = .{
             .nameservers_buffer = undefined,
             .nameservers_len = 0,
@@ -749,7 +293,7 @@ pub const ResolvConf = struct {
     const Directive = enum { options, nameserver, domain, search };
     const Option = enum { ndots, attempts, timeout };
 
-    fn parse(rc: *ResolvConf, io: Io, reader: *Io.Reader) !void {
+    pub fn parse(rc: *ResolvConf, io: Io, reader: *Io.Reader) !void {
         while (reader.takeSentinel('\n')) |line_with_comment| {
             const line = line: {
                 var split = std.mem.splitScalar(u8, line_with_comment, '#');
@@ -799,7 +343,7 @@ pub const ResolvConf = struct {
         rc.nameservers_len += 1;
     }
 
-    fn nameservers(rc: *const ResolvConf) []const IpAddress {
+    pub fn nameservers(rc: *const ResolvConf) []const IpAddress {
         return rc.nameservers_buffer[0..rc.nameservers_len];
     }
 };
lib/std/Io/EventLoop.zig
@@ -1410,7 +1410,7 @@ fn pread(userdata: ?*anyopaque, file: Io.File, buffer: []u8, offset: std.posix.o
         .NOMEM => return error.SystemResources,
         .NOTCONN => return error.SocketUnconnected,
         .CONNRESET => return error.ConnectionResetByPeer,
-        .TIMEDOUT => return error.ConnectionTimedOut,
+        .TIMEDOUT => return error.Timeout,
         .NXIO => return error.Unseekable,
         .SPIPE => return error.Unseekable,
         .OVERFLOW => return error.Unseekable,
lib/std/Io/File.zig
@@ -153,7 +153,7 @@ pub const ReadStreamingError = error{
     IsDir,
     BrokenPipe,
     ConnectionResetByPeer,
-    ConnectionTimedOut,
+    Timeout,
     NotOpenForReading,
     SocketUnconnected,
     /// This error occurs when no global event loop is configured,
lib/std/Io/net.zig
@@ -281,7 +281,6 @@ pub const IpAddress = union(enum) {
     }
 
     pub const ConnectError = error{
-        AddressInUse,
         AddressUnavailable,
         AddressFamilyUnsupported,
         /// Insufficient memory or other resource internal to the operating system.
@@ -291,7 +290,7 @@ pub const IpAddress = union(enum) {
         ConnectionResetByPeer,
         HostUnreachable,
         NetworkUnreachable,
-        ConnectionTimedOut,
+        Timeout,
         /// One of the `ConnectOptions` is not supported by the Io
         /// implementation.
         OptionUnsupported,
@@ -1165,7 +1164,7 @@ pub const Stream = struct {
             SystemResources,
             BrokenPipe,
             ConnectionResetByPeer,
-            ConnectionTimedOut,
+            Timeout,
             SocketUnconnected,
             /// The file descriptor does not hold the required rights to read
             /// from it.
lib/std/Io/Threaded.zig
@@ -8,6 +8,8 @@ const windows = std.os.windows;
 const std = @import("../std.zig");
 const Io = std.Io;
 const net = std.Io.net;
+const HostName = std.Io.net.HostName;
+const IpAddress = std.Io.net.IpAddress;
 const Allocator = std.mem.Allocator;
 const assert = std.debug.assert;
 const posix = std.posix;
@@ -156,9 +158,11 @@ pub fn io(pool: *Pool) Io {
             .groupCancel = groupCancel,
 
             .mutexLock = mutexLock,
+            .mutexLockUncancelable = mutexLockUncancelable,
             .mutexUnlock = mutexUnlock,
 
             .conditionWait = conditionWait,
+            .conditionWaitUncancelable = conditionWaitUncancelable,
             .conditionWake = conditionWake,
 
             .dirMake = switch (builtin.os.tag) {
@@ -235,6 +239,7 @@ pub fn io(pool: *Pool) Io {
             .netReceive = netReceive,
             .netInterfaceNameResolve = netInterfaceNameResolve,
             .netInterfaceName = netInterfaceName,
+            .netLookup = netLookup,
         },
     };
 }
@@ -653,26 +658,63 @@ fn checkCancel(pool: *Pool) error{Canceled}!void {
     if (cancelRequested(pool)) return error.Canceled;
 }
 
-fn mutexLock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) error{Canceled}!void {
+fn mutexLock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) Io.Cancelable!void {
+    const pool: *Pool = @ptrCast(@alignCast(userdata));
+    if (prev_state == .contended) {
+        try pool.checkCancel();
+        futexWait(@ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended));
+    }
+    while (@atomicRmw(Io.Mutex.State, &mutex.state, .Xchg, .contended, .acquire) != .unlocked) {
+        try pool.checkCancel();
+        futexWait(@ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended));
+    }
+}
+
+fn mutexLockUncancelable(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) void {
     _ = userdata;
     if (prev_state == .contended) {
-        std.Thread.Futex.wait(@ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended));
+        futexWait(@ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended));
     }
-    while (@atomicRmw(
-        Io.Mutex.State,
-        &mutex.state,
-        .Xchg,
-        .contended,
-        .acquire,
-    ) != .unlocked) {
-        std.Thread.Futex.wait(@ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended));
+    while (@atomicRmw(Io.Mutex.State, &mutex.state, .Xchg, .contended, .acquire) != .unlocked) {
+        futexWait(@ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended));
     }
 }
+
 fn mutexUnlock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) void {
     _ = userdata;
     _ = prev_state;
     if (@atomicRmw(Io.Mutex.State, &mutex.state, .Xchg, .unlocked, .release) == .contended) {
-        std.Thread.Futex.wake(@ptrCast(&mutex.state), 1);
+        futexWake(@ptrCast(&mutex.state), 1);
+    }
+}
+
+fn conditionWaitUncancelable(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) void {
+    const pool: *Pool = @ptrCast(@alignCast(userdata));
+    const pool_io = pool.io();
+    comptime assert(@TypeOf(cond.state) == u64);
+    const ints: *[2]std.atomic.Value(u32) = @ptrCast(&cond.state);
+    const cond_state = &ints[0];
+    const cond_epoch = &ints[1];
+    const one_waiter = 1;
+    const waiter_mask = 0xffff;
+    const one_signal = 1 << 16;
+    const signal_mask = 0xffff << 16;
+    var epoch = cond_epoch.load(.acquire);
+    var state = cond_state.fetchAdd(one_waiter, .monotonic);
+    assert(state & waiter_mask != waiter_mask);
+    state += one_waiter;
+
+    mutex.unlock(pool_io);
+    defer mutex.lockUncancelable(pool_io);
+
+    while (true) {
+        futexWait(cond_epoch, epoch);
+        epoch = cond_epoch.load(.acquire);
+        state = cond_state.load(.monotonic);
+        while (state & signal_mask != 0) {
+            const new_state = state - one_waiter - one_signal;
+            state = cond_state.cmpxchgWeak(state, new_state, .acquire, .monotonic) orelse return;
+        }
     }
 }
 
@@ -702,20 +744,18 @@ fn conditionWait(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) I
     state += one_waiter;
 
     mutex.unlock(pool.io());
-    defer mutex.lock(pool.io()) catch @panic("TODO");
-
-    var futex_deadline = std.Thread.Futex.Deadline.init(null);
+    defer mutex.lockUncancelable(pool.io());
 
     while (true) {
-        futex_deadline.wait(cond_epoch, epoch) catch |err| switch (err) {
-            error.Timeout => unreachable,
-        };
+        try pool.checkCancel();
+        futexWait(cond_epoch, epoch);
 
         epoch = cond_epoch.load(.acquire);
         state = cond_state.load(.monotonic);
 
-        // Try to wake up by consuming a signal and decremented the waiter we added previously.
-        // Acquire barrier ensures code before the wake() which added the signal happens before we decrement it and return.
+        // Try to wake up by consuming a signal and decremented the waiter we
+        // added previously. Acquire barrier ensures code before the wake()
+        // which added the signal happens before we decrement it and return.
         while (state & signal_mask != 0) {
             const new_state = state - one_waiter - one_signal;
             state = cond_state.cmpxchgWeak(state, new_state, .acquire, .monotonic) orelse return;
@@ -740,8 +780,10 @@ fn conditionWake(userdata: ?*anyopaque, cond: *Io.Condition, wake: Io.Condition.
         const signals = (state & signal_mask) / one_signal;
 
         // Reserves which waiters to wake up by incrementing the signals count.
-        // Therefore, the signals count is always less than or equal to the waiters count.
-        // We don't need to Futex.wake if there's nothing to wake up or if other wake() threads have reserved to wake up the current waiters.
+        // Therefore, the signals count is always less than or equal to the
+        // waiters count. We don't need to Futex.wake if there's nothing to
+        // wake up or if other wake() threads have reserved to wake up the
+        // current waiters.
         const wakeable = waiters - signals;
         if (wakeable == 0) {
             return;
@@ -752,16 +794,23 @@ fn conditionWake(userdata: ?*anyopaque, cond: *Io.Condition, wake: Io.Condition.
             .all => wakeable,
         };
 
-        // Reserve the amount of waiters to wake by incrementing the signals count.
-        // Release barrier ensures code before the wake() happens before the signal it posted and consumed by the wait() threads.
+        // Reserve the amount of waiters to wake by incrementing the signals
+        // count. Release barrier ensures code before the wake() happens before
+        // the signal it posted and consumed by the wait() threads.
         const new_state = state + (one_signal * to_wake);
         state = cond_state.cmpxchgWeak(state, new_state, .release, .monotonic) orelse {
             // Wake up the waiting threads we reserved above by changing the epoch value.
-            // NOTE: a waiting thread could miss a wake up if *exactly* ((1<<32)-1) wake()s happen between it observing the epoch and sleeping on it.
-            // This is very unlikely due to how many precise amount of Futex.wake() calls that would be between the waiting thread's potential preemption.
             //
-            // Release barrier ensures the signal being added to the state happens before the epoch is changed.
-            // If not, the waiting thread could potentially deadlock from missing both the state and epoch change:
+            // A waiting thread could miss a wake up if *exactly* ((1<<32)-1)
+            // wake()s happen between it observing the epoch and sleeping on
+            // it. This is very unlikely due to how many precise amount of
+            // Futex.wake() calls that would be between the waiting thread's
+            // potential preemption.
+            //
+            // Release barrier ensures the signal being added to the state
+            // happens before the epoch is changed. If not, the waiting thread
+            // could potentially deadlock from missing both the state and epoch
+            // change:
             //
             // - T2: UPDATE(&epoch, 1) (reordered before the state change)
             // - T1: e = LOAD(&epoch)
@@ -769,7 +818,7 @@ fn conditionWake(userdata: ?*anyopaque, cond: *Io.Condition, wake: Io.Condition.
             // - T2: UPDATE(&state, signal) + FUTEX_WAKE(&epoch)
             // - T1: s & signals == 0 -> FUTEX_WAIT(&epoch, e) (missed both epoch change and state change)
             _ = cond_epoch.fetchAdd(1, .release);
-            std.Thread.Futex.wake(cond_epoch, to_wake);
+            futexWake(cond_epoch, to_wake);
             return;
         };
     }
@@ -1298,7 +1347,7 @@ fn fileReadStreaming(userdata: ?*anyopaque, file: Io.File, data: [][]u8) Io.File
             .NOMEM => return error.SystemResources,
             .NOTCONN => return error.SocketUnconnected,
             .CONNRESET => return error.ConnectionResetByPeer,
-            .TIMEDOUT => return error.ConnectionTimedOut,
+            .TIMEDOUT => return error.Timeout,
             .NOTCAPABLE => return error.AccessDenied,
             else => |err| return posix.unexpectedErrno(err),
         }
@@ -1321,7 +1370,7 @@ fn fileReadStreaming(userdata: ?*anyopaque, file: Io.File, data: [][]u8) Io.File
             .NOMEM => return error.SystemResources,
             .NOTCONN => return error.SocketUnconnected,
             .CONNRESET => return error.ConnectionResetByPeer,
-            .TIMEDOUT => return error.ConnectionTimedOut,
+            .TIMEDOUT => return error.Timeout,
             else => |err| return posix.unexpectedErrno(err),
         }
     }
@@ -1420,7 +1469,7 @@ fn fileReadPositional(userdata: ?*anyopaque, file: Io.File, data: [][]u8, offset
             .NOMEM => return error.SystemResources,
             .NOTCONN => return error.SocketUnconnected,
             .CONNRESET => return error.ConnectionResetByPeer,
-            .TIMEDOUT => return error.ConnectionTimedOut,
+            .TIMEDOUT => return error.Timeout,
             .NXIO => return error.Unseekable,
             .SPIPE => return error.Unseekable,
             .OVERFLOW => return error.Unseekable,
@@ -1446,7 +1495,7 @@ fn fileReadPositional(userdata: ?*anyopaque, file: Io.File, data: [][]u8, offset
             .NOMEM => return error.SystemResources,
             .NOTCONN => return error.SocketUnconnected,
             .CONNRESET => return error.ConnectionResetByPeer,
-            .TIMEDOUT => return error.ConnectionTimedOut,
+            .TIMEDOUT => return error.Timeout,
             .NXIO => return error.Unseekable,
             .SPIPE => return error.Unseekable,
             .OVERFLOW => return error.Unseekable,
@@ -1693,9 +1742,9 @@ fn select(userdata: ?*anyopaque, futures: []const *Io.AnyFuture) usize {
 
 fn netListenIpPosix(
     userdata: ?*anyopaque,
-    address: net.IpAddress,
-    options: net.IpAddress.ListenOptions,
-) net.IpAddress.ListenError!net.Server {
+    address: IpAddress,
+    options: IpAddress.ListenOptions,
+) IpAddress.ListenError!net.Server {
     const pool: *Pool = @ptrCast(@alignCast(userdata));
     const family = posixAddressFamily(&address);
     const socket_fd = try openSocketPosix(pool, family, .{
@@ -1831,7 +1880,7 @@ fn posixConnect(pool: *Pool, socket_fd: posix.socket_t, addr: *const posix.socka
             .NETUNREACH => return error.NetworkUnreachable,
             .NOTSOCK => |err| return errnoBug(err),
             .PROTOTYPE => |err| return errnoBug(err),
-            .TIMEDOUT => return error.ConnectionTimedOut,
+            .TIMEDOUT => return error.Timeout,
             .CONNABORTED => |err| return errnoBug(err),
             .ACCES => return error.AccessDenied,
             .PERM => |err| return errnoBug(err),
@@ -1904,9 +1953,9 @@ fn setSocketOption(pool: *Pool, fd: posix.fd_t, level: i32, opt_name: u32, optio
 
 fn netConnectIpPosix(
     userdata: ?*anyopaque,
-    address: *const net.IpAddress,
-    options: net.IpAddress.ConnectOptions,
-) net.IpAddress.ConnectError!net.Stream {
+    address: *const IpAddress,
+    options: IpAddress.ConnectOptions,
+) IpAddress.ConnectError!net.Stream {
     if (options.timeout != .none) @panic("TODO");
     const pool: *Pool = @ptrCast(@alignCast(userdata));
     const family = posixAddressFamily(address);
@@ -1941,9 +1990,9 @@ fn netConnectUnix(
 
 fn netBindIpPosix(
     userdata: ?*anyopaque,
-    address: *const net.IpAddress,
-    options: net.IpAddress.BindOptions,
-) net.IpAddress.BindError!net.Socket {
+    address: *const IpAddress,
+    options: IpAddress.BindOptions,
+) IpAddress.BindError!net.Socket {
     const pool: *Pool = @ptrCast(@alignCast(userdata));
     const family = posixAddressFamily(address);
     const socket_fd = try openSocketPosix(pool, family, options);
@@ -1958,7 +2007,7 @@ fn netBindIpPosix(
     };
 }
 
-fn openSocketPosix(pool: *Pool, family: posix.sa_family_t, options: net.IpAddress.BindOptions) !posix.socket_t {
+fn openSocketPosix(pool: *Pool, family: posix.sa_family_t, options: IpAddress.BindOptions) !posix.socket_t {
     const mode = posixSocketMode(options.mode);
     const protocol = posixProtocol(options.protocol);
     const socket_fd = while (true) {
@@ -2081,7 +2130,7 @@ fn netReadPosix(userdata: ?*anyopaque, fd: net.Socket.Handle, data: [][]u8) net.
             .NOMEM => return error.SystemResources,
             .NOTCONN => return error.SocketUnconnected,
             .CONNRESET => return error.ConnectionResetByPeer,
-            .TIMEDOUT => return error.ConnectionTimedOut,
+            .TIMEDOUT => return error.Timeout,
             .NOTCAPABLE => return error.AccessDenied,
             else => |err| return posix.unexpectedErrno(err),
         }
@@ -2102,7 +2151,7 @@ fn netReadPosix(userdata: ?*anyopaque, fd: net.Socket.Handle, data: [][]u8) net.
             .NOMEM => return error.SystemResources,
             .NOTCONN => return error.SocketUnconnected,
             .CONNRESET => return error.ConnectionResetByPeer,
-            .TIMEDOUT => return error.ConnectionTimedOut,
+            .TIMEDOUT => return error.Timeout,
             .PIPE => return error.BrokenPipe,
             .NETDOWN => return error.NetworkDown,
             else => |err| return posix.unexpectedErrno(err),
@@ -2563,6 +2612,118 @@ fn netInterfaceName(userdata: ?*anyopaque, interface: net.Interface) net.Interfa
     @panic("unimplemented");
 }
 
+fn netLookup(
+    userdata: ?*anyopaque,
+    host_name: HostName,
+    resolved: *Io.Queue(HostName.LookupResult),
+    options: HostName.LookupOptions,
+) void {
+    const pool: *Pool = @ptrCast(@alignCast(userdata));
+    const pool_io = pool.io();
+    resolved.putOneUncancelable(pool_io, .{ .end = netLookupFallible(pool, host_name, resolved, options) });
+}
+
+fn netLookupFallible(
+    pool: *Pool,
+    host_name: HostName,
+    resolved: *Io.Queue(HostName.LookupResult),
+    options: HostName.LookupOptions,
+) !void {
+    const pool_io = pool.io();
+    const name = host_name.bytes;
+    assert(name.len <= HostName.max_len);
+
+    if (is_windows) {
+        // TODO use GetAddrInfoExW / GetAddrInfoExCancel
+        @compileError("TODO");
+    }
+
+    // On Linux, glibc provides getaddrinfo_a which is capable of supporting our semantics.
+    // However, musl's POSIX-compliant getaddrinfo is not, so we bypass it.
+
+    if (builtin.target.isGnuLibC()) {
+        // TODO use getaddrinfo_a / gai_cancel
+    }
+
+    if (native_os == .linux) {
+        if (options.family != .ip4) {
+            if (IpAddress.parseIp6(name, options.port)) |addr| {
+                try resolved.putAll(pool_io, &.{
+                    .{ .address = addr },
+                    .{ .canonical_name = copyCanon(options.canonical_name_buffer, name) },
+                });
+                return;
+            } else |_| {}
+        }
+
+        if (options.family != .ip6) {
+            if (IpAddress.parseIp4(name, options.port)) |addr| {
+                try resolved.putAll(pool_io, &.{
+                    .{ .address = addr },
+                    .{ .canonical_name = copyCanon(options.canonical_name_buffer, name) },
+                });
+            } else |_| {}
+        }
+
+        lookupHosts(pool, host_name, resolved, options) catch |err| switch (err) {
+            error.UnknownHostName => {},
+            else => |e| return e,
+        };
+
+        // RFC 6761 Section 6.3.3
+        // Name resolution APIs and libraries SHOULD recognize
+        // localhost names as special and SHOULD always return the IP
+        // loopback address for address queries and negative responses
+        // for all other query types.
+
+        // Check for equal to "localhost(.)" or ends in ".localhost(.)"
+        const localhost = if (name[name.len - 1] == '.') "localhost." else "localhost";
+        if (std.mem.endsWith(u8, name, localhost) and
+            (name.len == localhost.len or name[name.len - localhost.len] == '.'))
+        {
+            var results_buffer: [3]HostName.LookupResult = undefined;
+            var results_index: usize = 0;
+            if (options.family != .ip4) {
+                results_buffer[results_index] = .{ .address = .{ .ip6 = .loopback(options.port) } };
+                results_index += 1;
+            }
+            if (options.family != .ip6) {
+                results_buffer[results_index] = .{ .address = .{ .ip4 = .loopback(options.port) } };
+                results_index += 1;
+            }
+            const canon_name = "localhost";
+            const canon_name_dest = options.canonical_name_buffer[0..canon_name.len];
+            canon_name_dest.* = canon_name.*;
+            results_buffer[results_index] = .{ .canonical_name = .{ .bytes = canon_name_dest } };
+            results_index += 1;
+            try resolved.putAll(pool_io, results_buffer[0..results_index]);
+            return;
+        }
+
+        return lookupDnsSearch(pool, host_name, resolved, options);
+    }
+
+    if (native_os == .openbsd) {
+        // TODO use getaddrinfo_async / asr_abort
+    }
+
+    if (native_os == .freebsd) {
+        // TODO use dnsres_getaddrinfo
+    }
+
+    if (native_os.isDarwin()) {
+        // TODO use CFHostStartInfoResolution / CFHostCancelInfoResolution
+    }
+
+    if (builtin.link_libc) {
+        // This operating system lacks a way to resolve asynchronously. We are
+        // stuck with getaddrinfo.
+        @compileError("TODO");
+    }
+
+    return error.OptionUnsupported;
+}
+
 const PosixAddress = extern union {
     any: posix.sockaddr,
     in: posix.sockaddr.in,
@@ -2574,14 +2735,14 @@ const UnixAddress = extern union {
     un: posix.sockaddr.un,
 };
 
-fn posixAddressFamily(a: *const net.IpAddress) posix.sa_family_t {
+fn posixAddressFamily(a: *const IpAddress) posix.sa_family_t {
     return switch (a.*) {
         .ip4 => posix.AF.INET,
         .ip6 => posix.AF.INET6,
     };
 }
 
-fn addressFromPosix(posix_address: *PosixAddress) net.IpAddress {
+fn addressFromPosix(posix_address: *PosixAddress) IpAddress {
     return switch (posix_address.any.family) {
         posix.AF.INET => .{ .ip4 = address4FromPosix(&posix_address.in) },
         posix.AF.INET6 => .{ .ip6 = address6FromPosix(&posix_address.in6) },
@@ -2589,7 +2750,7 @@ fn addressFromPosix(posix_address: *PosixAddress) net.IpAddress {
     };
 }
 
-fn addressToPosix(a: *const net.IpAddress, storage: *PosixAddress) posix.socklen_t {
+fn addressToPosix(a: *const IpAddress, storage: *PosixAddress) posix.socklen_t {
     return switch (a.*) {
         .ip4 => |ip4| {
             storage.in = address4ToPosix(ip4);
@@ -2789,3 +2950,436 @@ fn pathToPosix(file_path: []const u8, buffer: *[posix.PATH_MAX]u8) Io.Dir.PathNa
     buffer[file_path.len] = 0;
     return buffer[0..file_path.len :0];
 }
+
+fn lookupDnsSearch(
+    pool: *Pool,
+    host_name: HostName,
+    resolved: *Io.Queue(HostName.LookupResult),
+    options: HostName.LookupOptions,
+) HostName.LookupError!void {
+    const pool_io = pool.io();
+    const rc = HostName.ResolvConf.init(pool_io) catch return error.ResolvConfParseFailed;
+
+    // Count dots, suppress search when >=ndots or name ends in
+    // a dot, which is an explicit request for global scope.
+    const dots = std.mem.countScalar(u8, host_name.bytes, '.');
+    const search_len = if (dots >= rc.ndots or std.mem.endsWith(u8, host_name.bytes, ".")) 0 else rc.search_len;
+    const search = rc.search_buffer[0..search_len];
+
+    var canon_name = host_name.bytes;
+
+    // Strip final dot for canon, fail if multiple trailing dots.
+    if (std.mem.endsWith(u8, canon_name, ".")) canon_name.len -= 1;
+    if (std.mem.endsWith(u8, canon_name, ".")) return error.UnknownHostName;
+
+    // Name with search domain appended is set up in `canon_name`. This
+    // both provides the desired default canonical name (if the requested
+    // name is not a CNAME record) and serves as a buffer for passing the
+    // full requested name to `lookupDns`.
+    @memcpy(options.canonical_name_buffer[0..canon_name.len], canon_name);
+    options.canonical_name_buffer[canon_name.len] = '.';
+    var it = std.mem.tokenizeAny(u8, search, " \t");
+    while (it.next()) |token| {
+        @memcpy(options.canonical_name_buffer[canon_name.len + 1 ..][0..token.len], token);
+        const lookup_canon_name = options.canonical_name_buffer[0 .. canon_name.len + 1 + token.len];
+        if (lookupDns(pool, lookup_canon_name, &rc, resolved, options)) |result| {
+            return result;
+        } else |err| switch (err) {
+            error.UnknownHostName => continue,
+            else => |e| return e,
+        }
+    }
+
+    const lookup_canon_name = options.canonical_name_buffer[0..canon_name.len];
+    return lookupDns(pool, lookup_canon_name, &rc, resolved, options);
+}
+
+fn lookupDns(
+    pool: *Pool,
+    lookup_canon_name: []const u8,
+    rc: *const HostName.ResolvConf,
+    resolved: *Io.Queue(HostName.LookupResult),
+    options: HostName.LookupOptions,
+) HostName.LookupError!void {
+    const pool_io = pool.io();
+    const family_records: [2]struct { af: IpAddress.Family, rr: u8 } = .{
+        .{ .af = .ip6, .rr = std.posix.RR.A },
+        .{ .af = .ip4, .rr = std.posix.RR.AAAA },
+    };
+    var query_buffers: [2][280]u8 = undefined;
+    var answer_buffer: [2 * 512]u8 = undefined;
+    var queries_buffer: [2][]const u8 = undefined;
+    var answers_buffer: [2][]const u8 = undefined;
+    var nq: usize = 0;
+    var answer_buffer_i: usize = 0;
+
+    for (family_records) |fr| {
+        if (options.family != fr.af) {
+            const entropy = std.crypto.random.array(u8, 2);
+            const len = writeResolutionQuery(&query_buffers[nq], 0, lookup_canon_name, 1, fr.rr, entropy);
+            queries_buffer[nq] = query_buffers[nq][0..len];
+            nq += 1;
+        }
+    }
+
+    var ip4_mapped: [HostName.ResolvConf.max_nameservers]IpAddress = undefined;
+    var any_ip6 = false;
+    for (rc.nameservers(), &ip4_mapped) |*ns, *m| {
+        m.* = .{ .ip6 = .fromAny(ns.*) };
+        any_ip6 = any_ip6 or ns.* == .ip6;
+    }
+    var socket = s: {
+        if (any_ip6) ip6: {
+            const ip6_addr: IpAddress = .{ .ip6 = .unspecified(0) };
+            const socket = ip6_addr.bind(pool_io, .{ .ip6_only = true, .mode = .dgram }) catch |err| switch (err) {
+                error.AddressFamilyUnsupported => break :ip6,
+                else => |e| return e,
+            };
+            break :s socket;
+        }
+        any_ip6 = false;
+        const ip4_addr: IpAddress = .{ .ip4 = .unspecified(0) };
+        const socket = try ip4_addr.bind(pool_io, .{ .mode = .dgram });
+        break :s socket;
+    };
+    defer socket.close(pool_io);
+
+    const mapped_nameservers = if (any_ip6) ip4_mapped[0..rc.nameservers_len] else rc.nameservers();
+    const queries = queries_buffer[0..nq];
+    const answers = answers_buffer[0..queries.len];
+    var answers_remaining = answers.len;
+    for (answers) |*answer| answer.len = 0;
+
+    // boot clock is chosen because time the computer is suspended should count
+    // against time spent waiting for external messages to arrive.
+    const clock: Io.Clock = .boot;
+    var now_ts = try clock.now(pool_io);
+    const final_ts = now_ts.addDuration(.fromSeconds(rc.timeout_seconds));
+    const attempt_duration: Io.Duration = .{
+        .nanoseconds = std.time.ns_per_s * @as(usize, rc.timeout_seconds) / rc.attempts,
+    };
+
+    send: while (now_ts.nanoseconds < final_ts.nanoseconds) : (now_ts = try clock.now(pool_io)) {
+        const max_messages = queries_buffer.len * HostName.ResolvConf.max_nameservers;
+        {
+            var message_buffer: [max_messages]Io.net.OutgoingMessage = undefined;
+            var message_i: usize = 0;
+            for (queries, answers) |query, *answer| {
+                if (answer.len != 0) continue;
+                for (mapped_nameservers) |*ns| {
+                    message_buffer[message_i] = .{
+                        .address = ns,
+                        .data_ptr = query.ptr,
+                        .data_len = query.len,
+                    };
+                    message_i += 1;
+                }
+            }
+            _ = netSend(pool, socket.handle, message_buffer[0..message_i], .{});
+        }
+
+        const timeout: Io.Timeout = .{ .deadline = .{
+            .raw = now_ts.addDuration(attempt_duration),
+            .clock = clock,
+        } };
+
+        while (true) {
+            var message_buffer: [max_messages]Io.net.IncomingMessage = undefined;
+            const buf = answer_buffer[answer_buffer_i..];
+            const recv_err, const recv_n = socket.receiveManyTimeout(pool_io, &message_buffer, buf, .{}, timeout);
+            for (message_buffer[0..recv_n]) |*received_message| {
+                const reply = received_message.data;
+                // Ignore non-identifiable packets.
+                if (reply.len < 4) continue;
+
+                // Ignore replies from addresses we didn't send to.
+                const ns = for (mapped_nameservers) |*ns| {
+                    if (received_message.from.eql(ns)) break ns;
+                } else {
+                    continue;
+                };
+
+                // Find which query this answer goes with, if any.
+                const query, const answer = for (queries, answers) |query, *answer| {
+                    if (reply[0] == query[0] and reply[1] == query[1]) break .{ query, answer };
+                } else {
+                    continue;
+                };
+                if (answer.len != 0) continue;
+
+                // Only accept positive or negative responses; retry immediately on
+                // server failure, and ignore all other codes such as refusal.
+                switch (reply[3] & 15) {
+                    0, 3 => {
+                        answer.* = reply;
+                        answer_buffer_i += reply.len;
+                        answers_remaining -= 1;
+                        if (answer_buffer.len - answer_buffer_i == 0) break :send;
+                        if (answers_remaining == 0) break :send;
+                    },
+                    2 => {
+                        var retry_message: Io.net.OutgoingMessage = .{
+                            .address = ns,
+                            .data_ptr = query.ptr,
+                            .data_len = query.len,
+                        };
+                        _ = netSend(pool, socket.handle, (&retry_message)[0..1], .{});
+                        continue;
+                    },
+                    else => continue,
+                }
+            }
+            if (recv_err) |err| switch (err) {
+                error.Canceled => return error.Canceled,
+                error.Timeout => continue :send,
+                else => continue,
+            };
+        }
+    } else {
+        return error.NameServerFailure;
+    }
+
+    var addresses_len: usize = 0;
+    var canonical_name: ?HostName = null;
+
+    for (answers) |answer| {
+        var it = HostName.DnsResponse.init(answer) catch {
+            // TODO accept a diagnostics struct and append warnings
+            continue;
+        };
+        while (it.next() catch {
+            // TODO accept a diagnostics struct and append warnings
+            continue;
+        }) |record| switch (record.rr) {
+            std.posix.RR.A => {
+                const data = record.packet[record.data_off..][0..record.data_len];
+                if (data.len != 4) return error.InvalidDnsARecord;
+                try resolved.putOne(pool_io, .{ .address = .{ .ip4 = .{
+                    .bytes = data[0..4].*,
+                    .port = options.port,
+                } } });
+                addresses_len += 1;
+            },
+            std.posix.RR.AAAA => {
+                const data = record.packet[record.data_off..][0..record.data_len];
+                if (data.len != 16) return error.InvalidDnsAAAARecord;
+                try resolved.putOne(pool_io, .{ .address = .{ .ip6 = .{
+                    .bytes = data[0..16].*,
+                    .port = options.port,
+                } } });
+                addresses_len += 1;
+            },
+            std.posix.RR.CNAME => {
+                _, canonical_name = HostName.expand(record.packet, record.data_off, options.canonical_name_buffer) catch
+                    return error.InvalidDnsCnameRecord;
+            },
+            else => continue,
+        };
+    }
+
+    try resolved.putOne(pool_io, .{ .canonical_name = canonical_name orelse .{ .bytes = lookup_canon_name } });
+    if (addresses_len == 0) return error.NameServerFailure;
+}
+
+fn lookupHosts(
+    pool: *Pool,
+    host_name: HostName,
+    resolved: *Io.Queue(HostName.LookupResult),
+    options: HostName.LookupOptions,
+) !void {
+    const pool_io = pool.io();
+    const file = Io.File.openAbsolute(pool_io, "/etc/hosts", .{}) catch |err| switch (err) {
+        error.FileNotFound,
+        error.NotDir,
+        error.AccessDenied,
+        => return error.UnknownHostName,
+
+        error.Canceled => |e| return e,
+
+        else => {
+            // TODO populate optional diagnostic struct
+            return error.DetectingNetworkConfigurationFailed;
+        },
+    };
+    defer file.close(pool_io);
+
+    var line_buf: [512]u8 = undefined;
+    var file_reader = file.reader(pool_io, &line_buf);
+    return lookupHostsReader(pool, host_name, resolved, options, &file_reader.interface) catch |err| switch (err) {
+        error.ReadFailed => switch (file_reader.err.?) {
+            error.Canceled => |e| return e,
+            else => {
+                // TODO populate optional diagnostic struct
+                return error.DetectingNetworkConfigurationFailed;
+            },
+        },
+        error.Canceled => |e| return e,
+        error.UnknownHostName => |e| return e,
+    };
+}
+
+fn lookupHostsReader(
+    pool: *Pool,
+    host_name: HostName,
+    resolved: *Io.Queue(HostName.LookupResult),
+    options: HostName.LookupOptions,
+    reader: *Io.Reader,
+) error{ ReadFailed, Canceled, UnknownHostName }!void {
+    const pool_io = pool.io();
+    var addresses_len: usize = 0;
+    var canonical_name: ?HostName = null;
+    while (true) {
+        const line = reader.takeDelimiterExclusive('\n') catch |err| switch (err) {
+            error.StreamTooLong => {
+                // Skip lines that are too long.
+                _ = reader.discardDelimiterInclusive('\n') catch |e| switch (e) {
+                    error.EndOfStream => break,
+                    error.ReadFailed => return error.ReadFailed,
+                };
+                continue;
+            },
+            error.ReadFailed => return error.ReadFailed,
+            error.EndOfStream => break,
+        };
+        reader.toss(1);
+        var split_it = std.mem.splitScalar(u8, line, '#');
+        const no_comment_line = split_it.first();
+
+        var line_it = std.mem.tokenizeAny(u8, no_comment_line, " \t");
+        const ip_text = line_it.next() orelse continue;
+        var first_name_text: ?[]const u8 = null;
+        while (line_it.next()) |name_text| {
+            if (std.mem.eql(u8, name_text, host_name.bytes)) {
+                if (first_name_text == null) first_name_text = name_text;
+                break;
+            }
+        } else continue;
+
+        if (canonical_name == null) {
+            if (HostName.init(first_name_text.?)) |name_text| {
+                if (name_text.bytes.len <= options.canonical_name_buffer.len) {
+                    const canonical_name_dest = options.canonical_name_buffer[0..name_text.bytes.len];
+                    @memcpy(canonical_name_dest, name_text.bytes);
+                    canonical_name = .{ .bytes = canonical_name_dest };
+                }
+            } else |_| {}
+        }
+
+        if (options.family != .ip6) {
+            if (IpAddress.parseIp4(ip_text, options.port)) |addr| {
+                try resolved.putOne(pool_io, .{ .address = addr });
+                addresses_len += 1;
+            } else |_| {}
+        }
+        if (options.family != .ip4) {
+            if (IpAddress.parseIp6(ip_text, options.port)) |addr| {
+                try resolved.putOne(pool_io, .{ .address = addr });
+                addresses_len += 1;
+            } else |_| {}
+        }
+    }
+
+    if (canonical_name) |canon_name| try resolved.putOne(pool_io, .{ .canonical_name = canon_name });
+    if (addresses_len == 0) return error.UnknownHostName;
+}
+
+/// Writes DNS resolution query packet data to `w`; at most 280 bytes.
+fn writeResolutionQuery(q: *[280]u8, op: u4, dname: []const u8, class: u8, ty: u8, entropy: [2]u8) usize {
+    // This implementation is ported from musl libc.
+    // A more idiomatic "ziggy" implementation would be welcome.
+    var name = dname;
+    if (std.mem.endsWith(u8, name, ".")) name.len -= 1;
+    assert(name.len <= 253);
+    const n = 17 + name.len + @intFromBool(name.len != 0);
+
+    // Construct query template - ID will be filled later
+    q[0..2].* = entropy;
+    @memset(q[2..n], 0);
+    q[2] = @as(u8, op) * 8 + 1;
+    q[5] = 1;
+    @memcpy(q[13..][0..name.len], name);
+    var i: usize = 13;
+    var j: usize = undefined;
+    while (q[i] != 0) : (i = j + 1) {
+        j = i;
+        while (q[j] != 0 and q[j] != '.') : (j += 1) {}
+        // TODO determine the circumstances for this and whether or
+        // not this should be an error.
+        if (j - i - 1 > 62) unreachable;
+        q[i - 1] = @intCast(j - i);
+    }
+    q[i + 1] = ty;
+    q[i + 3] = class;
+    return n;
+}
+
+fn copyCanon(canonical_name_buffer: *[HostName.max_len]u8, name: []const u8) HostName {
+    const dest = canonical_name_buffer[0..name.len];
+    @memcpy(dest, name);
+    return .{ .bytes = dest };
+}
+
+pub fn futexWait(ptr: *const std.atomic.Value(u32), expect: u32) void {
+    @branchHint(.cold);
+
+    if (native_os == .linux) {
+        const linux = std.os.linux;
+        const rc = linux.futex_4arg(ptr, .{ .cmd = .WAIT, .private = true }, expect, null);
+        if (builtin.mode == .Debug) switch (linux.E.init(rc)) {
+            .SUCCESS => {}, // notified by `wake()`
+            .INTR => {}, // gives caller a chance to check cancellation
+            .AGAIN => {}, // ptr.* != expect
+            .INVAL => {}, // possibly timeout overflow
+            .TIMEDOUT => unreachable,
+            .FAULT => unreachable, // ptr was invalid
+            else => unreachable,
+        };
+        return;
+    }
+
+    @compileError("TODO");
+}
+
+pub fn futexWaitDuration(ptr: *const std.atomic.Value(u32), expect: u32, timeout: Io.Duration) void {
+    @branchHint(.cold);
+
+    if (native_os == .linux) {
+        const linux = std.os.linux;
+        var ts = timestampToPosix(timeout.toNanoseconds());
+        const rc = linux.futex_4arg(ptr, .{ .cmd = .WAIT, .private = true }, expect, &ts);
+        if (builtin.mode == .Debug) switch (linux.E.init(rc)) {
+            .SUCCESS => {}, // notified by `wake()`
+            .INTR => {}, // gives caller a chance to check cancellation
+            .AGAIN => {}, // ptr.* != expect
+            .TIMEDOUT => {},
+            .INVAL => {}, // possibly timeout overflow
+            .FAULT => unreachable, // ptr was invalid
+            else => unreachable,
+        };
+        return;
+    }
+
+    @compileError("TODO");
+}
+
+pub fn futexWake(ptr: *const std.atomic.Value(u32), max_waiters: u32) void {
+    @branchHint(.cold);
+
+    if (native_os == .linux) {
+        const linux = std.os.linux;
+        const rc = linux.futex_3arg(
+            &ptr.raw,
+            .{ .cmd = .WAKE, .private = true },
+            @min(max_waiters, std.math.maxInt(i32)),
+        );
+        if (builtin.mode == .Debug) switch (linux.E.init(rc)) {
+            .SUCCESS => {}, // successful wake up
+            .INVAL => {}, // invalid futex_wait() on ptr done elsewhere
+            .FAULT => {}, // pointer became invalid while doing the wake
+            else => unreachable,
+        };
+        return;
+    }
+
+    @compileError("TODO");
+}
lib/std/zig/system.zig
@@ -428,7 +428,7 @@ pub fn resolveTargetQuery(io: Io, query: Target.Query) DetectError!Target {
         error.WouldBlock => return error.Unexpected,
         error.BrokenPipe => return error.Unexpected,
         error.ConnectionResetByPeer => return error.Unexpected,
-        error.ConnectionTimedOut => return error.Unexpected,
+        error.Timeout => return error.Unexpected,
         error.NotOpenForReading => return error.Unexpected,
         error.SocketUnconnected => return error.Unexpected,
 
lib/std/Io.zig
@@ -649,9 +649,11 @@ pub const VTable = struct {
     select: *const fn (?*anyopaque, futures: []const *AnyFuture) usize,
 
     mutexLock: *const fn (?*anyopaque, prev_state: Mutex.State, mutex: *Mutex) Cancelable!void,
+    mutexLockUncancelable: *const fn (?*anyopaque, prev_state: Mutex.State, mutex: *Mutex) void,
     mutexUnlock: *const fn (?*anyopaque, prev_state: Mutex.State, mutex: *Mutex) void,
 
     conditionWait: *const fn (?*anyopaque, cond: *Condition, mutex: *Mutex) Cancelable!void,
+    conditionWaitUncancelable: *const fn (?*anyopaque, cond: *Condition, mutex: *Mutex) void,
     conditionWake: *const fn (?*anyopaque, cond: *Condition, wake: Condition.Wake) void,
 
     dirMake: *const fn (?*anyopaque, Dir, sub_path: []const u8, mode: Dir.Mode) Dir.MakeError!void,
@@ -686,6 +688,7 @@ pub const VTable = struct {
     netClose: *const fn (?*anyopaque, handle: net.Socket.Handle) void,
     netInterfaceNameResolve: *const fn (?*anyopaque, *const net.Interface.Name) net.Interface.Name.ResolveError!net.Interface,
     netInterfaceName: *const fn (?*anyopaque, net.Interface) net.Interface.NameError!net.Interface.Name,
+    netLookup: *const fn (?*anyopaque, net.HostName, *Queue(net.HostName.LookupResult), net.HostName.LookupOptions) void,
 };
 
 pub const Cancelable = error{
@@ -1030,7 +1033,7 @@ pub const Group = struct {
     }
 };
 
-pub const Mutex = if (true) struct {
+pub const Mutex = struct {
     state: State,
 
     pub const State = enum(usize) {
@@ -1073,54 +1076,32 @@ pub const Mutex = if (true) struct {
         return io.vtable.mutexLock(io.userdata, prev_state, mutex);
     }
 
+    /// Same as `lock` but cannot be canceled.
+    pub fn lockUncancelable(mutex: *Mutex, io: std.Io) void {
+        const prev_state: State = @enumFromInt(@atomicRmw(
+            usize,
+            @as(*usize, @ptrCast(&mutex.state)),
+            .And,
+            ~@intFromEnum(State.unlocked),
+            .acquire,
+        ));
+        if (prev_state.isUnlocked()) {
+            @branchHint(.likely);
+            return;
+        }
+        return io.vtable.mutexLockUncancelable(io.userdata, prev_state, mutex);
+    }
+
     pub fn unlock(mutex: *Mutex, io: std.Io) void {
         const prev_state = @cmpxchgWeak(State, &mutex.state, .locked_once, .unlocked, .release, .acquire) orelse {
             @branchHint(.likely);
             return;
         };
-        std.debug.assert(prev_state != .unlocked); // mutex not locked
+        assert(prev_state != .unlocked); // mutex not locked
         return io.vtable.mutexUnlock(io.userdata, prev_state, mutex);
     }
-} else struct {
-    state: std.atomic.Value(u32),
-
-    pub const State = void;
-
-    pub const init: Mutex = .{ .state = .init(unlocked) };
-
-    pub const unlocked: u32 = 0b00;
-    pub const locked: u32 = 0b01;
-    pub const contended: u32 = 0b11; // must contain the `locked` bit for x86 optimization below
-
-    pub fn tryLock(m: *Mutex) bool {
-        // On x86, use `lock bts` instead of `lock cmpxchg` as:
-        // - they both seem to mark the cache-line as modified regardless: https://stackoverflow.com/a/63350048
-        // - `lock bts` is smaller instruction-wise which makes it better for inlining
-        if (builtin.target.cpu.arch.isX86()) {
-            const locked_bit = @ctz(locked);
-            return m.state.bitSet(locked_bit, .acquire) == 0;
-        }
-
-        // Acquire barrier ensures grabbing the lock happens before the critical section
-        // and that the previous lock holder's critical section happens before we grab the lock.
-        return m.state.cmpxchgWeak(unlocked, locked, .acquire, .monotonic) == null;
-    }
-
-    /// Avoids the vtable for uncontended locks.
-    pub fn lock(m: *Mutex, io: Io) Cancelable!void {
-        if (!m.tryLock()) {
-            @branchHint(.unlikely);
-            try io.vtable.mutexLock(io.userdata, {}, m);
-        }
-    }
-
-    pub fn unlock(m: *Mutex, io: Io) void {
-        io.vtable.mutexUnlock(io.userdata, {}, m);
-    }
 };
 
-/// Supports exactly 1 waiter. More than 1 simultaneous wait on the same
-/// condition is illegal.
 pub const Condition = struct {
     state: u64 = 0,
 
@@ -1128,6 +1109,10 @@ pub const Condition = struct {
         return io.vtable.conditionWait(io.userdata, cond, mutex);
     }
 
+    pub fn waitUncancelable(cond: *Condition, io: Io, mutex: *Mutex) void {
+        return io.vtable.conditionWaitUncancelable(io.userdata, cond, mutex);
+    }
+
     pub fn signal(cond: *Condition, io: Io) void {
         io.vtable.conditionWake(io.userdata, cond, .one);
     }
@@ -1137,9 +1122,9 @@ pub const Condition = struct {
     }
 
     pub const Wake = enum {
-        /// wake up only one thread
+        /// Wake up only one thread.
         one,
-        /// wake up all thread
+        /// Wake up all threads.
         all,
     };
 };
@@ -1180,10 +1165,24 @@ pub const TypeErasedQueue = struct {
 
     pub fn put(q: *TypeErasedQueue, io: Io, elements: []const u8, min: usize) Cancelable!usize {
         assert(elements.len >= min);
-
+        if (elements.len == 0) return 0;
         try q.mutex.lock(io);
         defer q.mutex.unlock(io);
+        return putLocked(q, io, elements, min, false);
+    }
+
+    /// Same as `put` but cannot be canceled.
+    pub fn putUncancelable(q: *TypeErasedQueue, io: Io, elements: []const u8, min: usize) usize {
+        assert(elements.len >= min);
+        if (elements.len == 0) return 0;
+        q.mutex.lockUncancelable(io);
+        defer q.mutex.unlock(io);
+        return putLocked(q, io, elements, min, true) catch |err| switch (err) {
+            error.Canceled => unreachable,
+        };
+    }
 
+    fn putLocked(q: *TypeErasedQueue, io: Io, elements: []const u8, min: usize, uncancelable: bool) Cancelable!usize {
         // Getters have first priority on the data, and only when the getters
         // queue is empty do we start populating the buffer.
 
@@ -1226,7 +1225,10 @@ pub const TypeErasedQueue = struct {
 
             var pending: Put = .{ .remaining = remaining, .condition = .{}, .node = .{} };
             q.putters.append(&pending.node);
-            try pending.condition.wait(io, &q.mutex);
+            if (uncancelable)
+                pending.condition.waitUncancelable(io, &q.mutex)
+            else
+                try pending.condition.wait(io, &q.mutex);
             remaining = pending.remaining;
         }
     }
@@ -1347,6 +1349,16 @@ pub fn Queue(Elem: type) type {
             return @divExact(try q.type_erased.put(io, @ptrCast(elements), min * @sizeOf(Elem)), @sizeOf(Elem));
         }
 
+        /// Same as `put` but blocks until all elements have been added to the queue.
+        pub fn putAll(q: *@This(), io: Io, elements: []const Elem) Cancelable!void {
+            assert(try q.put(io, elements, elements.len) == elements.len);
+        }
+
+        /// Same as `put` but cannot be interrupted.
+        pub fn putUncancelable(q: *@This(), io: Io, elements: []const Elem, min: usize) usize {
+            return @divExact(q.type_erased.putUncancelable(io, @ptrCast(elements), min * @sizeOf(Elem)), @sizeOf(Elem));
+        }
+
         /// Receives elements from the beginning of the queue. The function
         /// returns when at least `min` elements have been populated inside
         /// `buffer`.
@@ -1362,11 +1374,20 @@ pub fn Queue(Elem: type) type {
             assert(try q.put(io, &.{item}, 1) == 1);
         }
 
+        pub fn putOneUncancelable(q: *@This(), io: Io, item: Elem) void {
+            assert(q.putUncancelable(io, &.{item}, 1) == 1);
+        }
+
         pub fn getOne(q: *@This(), io: Io) Cancelable!Elem {
             var buf: [1]Elem = undefined;
             assert(try q.get(io, &buf, 1) == 1);
             return buf[0];
         }
+
+        /// Returns buffer length in `Elem` units.
+        pub fn capacity(q: *const @This()) usize {
+            return @divExact(q.type_erased.buffer.len, @sizeOf(Elem));
+        }
     };
 }
 
lib/std/posix.zig
@@ -845,7 +845,7 @@ pub fn read(fd: fd_t, buf: []u8) ReadError!usize {
             .NOMEM => return error.SystemResources,
             .NOTCONN => return error.SocketUnconnected,
             .CONNRESET => return error.ConnectionResetByPeer,
-            .TIMEDOUT => return error.ConnectionTimedOut,
+            .TIMEDOUT => return error.Timeout,
             .NOTCAPABLE => return error.AccessDenied,
             else => |err| return unexpectedErrno(err),
         }
@@ -874,7 +874,7 @@ pub fn read(fd: fd_t, buf: []u8) ReadError!usize {
             .NOMEM => return error.SystemResources,
             .NOTCONN => return error.SocketUnconnected,
             .CONNRESET => return error.ConnectionResetByPeer,
-            .TIMEDOUT => return error.ConnectionTimedOut,
+            .TIMEDOUT => return error.Timeout,
             else => |err| return unexpectedErrno(err),
         }
     }
@@ -914,7 +914,7 @@ pub fn readv(fd: fd_t, iov: []const iovec) ReadError!usize {
             .NOMEM => return error.SystemResources,
             .NOTCONN => return error.SocketUnconnected,
             .CONNRESET => return error.ConnectionResetByPeer,
-            .TIMEDOUT => return error.ConnectionTimedOut,
+            .TIMEDOUT => return error.Timeout,
             .NOTCAPABLE => return error.AccessDenied,
             else => |err| return unexpectedErrno(err),
         }
@@ -936,7 +936,7 @@ pub fn readv(fd: fd_t, iov: []const iovec) ReadError!usize {
             .NOMEM => return error.SystemResources,
             .NOTCONN => return error.SocketUnconnected,
             .CONNRESET => return error.ConnectionResetByPeer,
-            .TIMEDOUT => return error.ConnectionTimedOut,
+            .TIMEDOUT => return error.Timeout,
             else => |err| return unexpectedErrno(err),
         }
     }
@@ -983,7 +983,7 @@ pub fn pread(fd: fd_t, buf: []u8, offset: u64) PReadError!usize {
             .NOMEM => return error.SystemResources,
             .NOTCONN => return error.SocketUnconnected,
             .CONNRESET => return error.ConnectionResetByPeer,
-            .TIMEDOUT => return error.ConnectionTimedOut,
+            .TIMEDOUT => return error.Timeout,
             .NXIO => return error.Unseekable,
             .SPIPE => return error.Unseekable,
             .OVERFLOW => return error.Unseekable,
@@ -1016,7 +1016,7 @@ pub fn pread(fd: fd_t, buf: []u8, offset: u64) PReadError!usize {
             .NOMEM => return error.SystemResources,
             .NOTCONN => return error.SocketUnconnected,
             .CONNRESET => return error.ConnectionResetByPeer,
-            .TIMEDOUT => return error.ConnectionTimedOut,
+            .TIMEDOUT => return error.Timeout,
             .NXIO => return error.Unseekable,
             .SPIPE => return error.Unseekable,
             .OVERFLOW => return error.Unseekable,
@@ -1134,7 +1134,7 @@ pub fn preadv(fd: fd_t, iov: []const iovec, offset: u64) PReadError!usize {
             .NOMEM => return error.SystemResources,
             .NOTCONN => return error.SocketUnconnected,
             .CONNRESET => return error.ConnectionResetByPeer,
-            .TIMEDOUT => return error.ConnectionTimedOut,
+            .TIMEDOUT => return error.Timeout,
             .NXIO => return error.Unseekable,
             .SPIPE => return error.Unseekable,
             .OVERFLOW => return error.Unseekable,
@@ -1160,7 +1160,7 @@ pub fn preadv(fd: fd_t, iov: []const iovec, offset: u64) PReadError!usize {
             .NOMEM => return error.SystemResources,
             .NOTCONN => return error.SocketUnconnected,
             .CONNRESET => return error.ConnectionResetByPeer,
-            .TIMEDOUT => return error.ConnectionTimedOut,
+            .TIMEDOUT => return error.Timeout,
             .NXIO => return error.Unseekable,
             .SPIPE => return error.Unseekable,
             .OVERFLOW => return error.Unseekable,
@@ -4205,7 +4205,7 @@ pub const ConnectError = error{
 
     /// Timeout  while  attempting  connection.   The server may be too busy to accept new connections.  Note
     /// that for IP sockets the timeout may be very long when syncookies are enabled on the server.
-    ConnectionTimedOut,
+    Timeout,
 
     /// This error occurs when no global event loop is configured,
     /// and connecting to the socket would block.
@@ -4236,7 +4236,7 @@ pub fn connect(sock: socket_t, sock_addr: *const sockaddr, len: socklen_t) Conne
             .WSAEADDRNOTAVAIL => return error.AddressNotAvailable,
             .WSAECONNREFUSED => return error.ConnectionRefused,
             .WSAECONNRESET => return error.ConnectionResetByPeer,
-            .WSAETIMEDOUT => return error.ConnectionTimedOut,
+            .WSAETIMEDOUT => return error.Timeout,
             .WSAEHOSTUNREACH, // TODO: should we return NetworkUnreachable in this case as well?
             .WSAENETUNREACH,
             => return error.NetworkUnreachable,
@@ -4273,7 +4273,7 @@ pub fn connect(sock: socket_t, sock_addr: *const sockaddr, len: socklen_t) Conne
             .NETUNREACH => return error.NetworkUnreachable,
             .NOTSOCK => unreachable, // The file descriptor sockfd does not refer to a socket.
             .PROTOTYPE => unreachable, // The socket type does not support the requested communications protocol.
-            .TIMEDOUT => return error.ConnectionTimedOut,
+            .TIMEDOUT => return error.Timeout,
             .NOENT => return error.FileNotFound, // Returned when socket is AF.UNIX and the given path does not exist.
             .CONNABORTED => unreachable, // Tried to reuse socket that previously received error.ConnectionRefused.
             else => |err| return unexpectedErrno(err),
@@ -4333,7 +4333,7 @@ pub fn getsockoptError(sockfd: fd_t) ConnectError!void {
             .NETUNREACH => return error.NetworkUnreachable,
             .NOTSOCK => unreachable, // The file descriptor sockfd does not refer to a socket.
             .PROTOTYPE => unreachable, // The socket type does not support the requested communications protocol.
-            .TIMEDOUT => return error.ConnectionTimedOut,
+            .TIMEDOUT => return error.Timeout,
             .CONNRESET => return error.ConnectionResetByPeer,
             else => |err| return unexpectedErrno(err),
         },
@@ -6465,7 +6465,7 @@ pub const RecvFromError = error{
     SystemResources,
 
     ConnectionResetByPeer,
-    ConnectionTimedOut,
+    Timeout,
 
     /// The socket has not been bound.
     SocketNotBound,
@@ -6508,7 +6508,7 @@ pub fn recvfrom(
                     .WSAENETDOWN => return error.NetworkDown,
                     .WSAENOTCONN => return error.SocketUnconnected,
                     .WSAEWOULDBLOCK => return error.WouldBlock,
-                    .WSAETIMEDOUT => return error.ConnectionTimedOut,
+                    .WSAETIMEDOUT => return error.Timeout,
                     // TODO: handle more errors
                     else => |err| return windows.unexpectedWSAError(err),
                 }
@@ -6528,7 +6528,7 @@ pub fn recvfrom(
                 .NOMEM => return error.SystemResources,
                 .CONNREFUSED => return error.ConnectionRefused,
                 .CONNRESET => return error.ConnectionResetByPeer,
-                .TIMEDOUT => return error.ConnectionTimedOut,
+                .TIMEDOUT => return error.Timeout,
                 .PIPE => return error.BrokenPipe,
                 else => |err| return unexpectedErrno(err),
             }
BRANCH_TODO
@@ -1,3 +1,4 @@
+* Threaded: rename Pool to Threaded
 * Threaded: finish linux impl (all tests passing)
 * Threaded: finish macos impl 
 * Threaded: finish windows impl 
@@ -14,4 +15,6 @@
 * move fs.File.Writer to Io
 * add non-blocking flag to net and fs operations, handle EAGAIN
 * finish moving std.fs to Io
+* migrate child process into std.Io
+* eliminate std.Io.poll (it should be replaced by "select" functionality)
 * finish moving all of std.posix into Threaded