From e07da33a77f4d4bd4704726692d92db16d488cea Mon Sep 17 00:00:00 2001 From: hemisputnik Date: Sat, 26 Sep 2026 18:35:19 +0300 Subject: [PATCH 01/10] std.Io.Threaded: drop MSG_NOSIGNAL from recvmsg flags MSG_NOSIGNAL is not defined for recvmsg, only sendmsg. --- lib/std/Io/Threaded.zig | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 6c44b6ab73e1d53fab7c6c61a4b6ee391e6790ed..ebf728f59d72cde208c79ba91334b8f54a216f99 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -13229,7 +13229,6 @@ fn netReceivePosix( @as(u32, if (flags.oob) posix.MSG.OOB else 0) | @as(u32, if (flags.peek) posix.MSG.PEEK else 0) | @as(u32, if (flags.trunc) posix.MSG.TRUNC else 0) | - posix.MSG.NOSIGNAL | @as(u32, if (nonblocking) posix.MSG.DONTWAIT else 0); var storage: PosixAddress = undefined; -- 2.54.0 From 8909d25b365de94d4b37c613ce3276634765a86b Mon Sep 17 00:00:00 2001 From: hemisputnik Date: Wed, 22 Jul 2026 11:18:38 +0300 Subject: [PATCH 02/10] std.Io.Threaded: avoid passing undefined flags to kernel --- lib/std/Io/Threaded.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index ebf728f59d72cde208c79ba91334b8f54a216f99..da5adae30bf6d4683ec0ee85a1ef524e2398c52c 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -13240,7 +13240,7 @@ fn netReceivePosix( .iovlen = 1, .control = message.control.ptr, .controllen = @intCast(message.control.len), - .flags = undefined, + .flags = 0, }; const syscall = try Syscall.start(); -- 2.54.0 From 20355494085a6eb59b73816038a5d0c56b5492c6 Mon Sep 17 00:00:00 2001 From: hemisputnik Date: Wed, 22 Jul 2026 09:06:26 +0300 Subject: [PATCH 03/10] std.Io.net: improve reception of control data A couple of changes to improve reception of control data when using the datagram API. Instead of having a separate control buffer per received message, there's one big buffer that all messages share (like the data buffer). This means that now IncomingMessage has no user-initialised fields, and can be initialised with undefined (which means that .init is now deprecated). Also, instead of taking a pointer to mutable IncomingMessage, netReceiveOne* functions simply return a IncomingMessage. --- lib/std/Io.zig | 1 + lib/std/Io/Kqueue.zig | 2 + lib/std/Io/Threaded.zig | 26 ++++++++---- lib/std/Io/Uring.zig | 13 ++++-- lib/std/Io/net.zig | 87 +++++++++++++++++++++++++++++++++++------ 5 files changed, 106 insertions(+), 23 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index d83c63e7276615651ad896128e6878fcb0e8e792..7d43bef3bdd52f6e4bcbc17351b1e2c9107f25f0 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -346,6 +346,7 @@ pub const Operation = union(enum) { socket_handle: net.Socket.Handle, message_buffer: []net.IncomingMessage, data_buffer: []u8, + control_buffer: []u8 = &.{}, flags: net.ReceiveFlags, pub const Error = error{ diff --git a/lib/std/Io/Kqueue.zig b/lib/std/Io/Kqueue.zig index 8f8be91d59bd3f3131ad5c08955da9de09b90e26..f663ab366a157ea562809d58c4b03ef9f6861c76 100644 --- a/lib/std/Io/Kqueue.zig +++ b/lib/std/Io/Kqueue.zig @@ -1183,6 +1183,7 @@ fn netReceive( handle: net.Socket.Handle, message_buffer: []net.IncomingMessage, data_buffer: []u8, + control_buffer: []u8, flags: net.ReceiveFlags, timeout: Io.Timeout, ) struct { ?net.Socket.ReceiveTimeoutError, usize } { @@ -1191,6 +1192,7 @@ fn netReceive( _ = handle; _ = message_buffer; _ = data_buffer; + _ = control_buffer; _ = flags; _ = timeout; @panic("TODO"); diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index da5adae30bf6d4683ec0ee85a1ef524e2398c52c..33f9083bc54896d1c1741293eb38eb28033e8046 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -2555,8 +2555,8 @@ fn operate(userdata: ?*anyopaque, operation: Io.Operation) Io.Cancelable!Io.Oper .device_io_control => |*o| return .{ .device_io_control = try deviceIoControl(o) }, .net_receive => |*o| return .{ .net_receive = o: { if (!have_networking) break :o .{ error.NetworkDown, 0 }; - if (is_windows) break :o netReceiveWindows(t, o.socket_handle, o.message_buffer, o.data_buffer, o.flags); - netReceivePosix(o.socket_handle, &o.message_buffer[0], o.data_buffer, o.flags, false) catch |err| switch (err) { + if (is_windows) break :o netReceiveWindows(t, o.socket_handle, o.message_buffer, o.data_buffer, o.control_buffer, o.flags); + netReceiveOnePosix(o.socket_handle, &o.message_buffer[0], o.data_buffer, o.control_buffer, o.flags, false) catch |err| switch (err) { error.Canceled => |e| return e, error.WouldBlock => unreachable, else => |e| break :o .{ e, 0 }, @@ -2825,9 +2825,11 @@ fn batchAwaitConcurrent(userdata: ?*anyopaque, b: *Io.Batch, timeout: Io.Timeout .device_io_control => |o| try poll_storage.add(o.file.handle, posix.POLL.IN | posix.POLL.OUT | posix.POLL.ERR), .net_receive => |*o| nb: { var data_i: usize = 0; + var control_i: usize = 0; const result: Io.Operation.Result = .{ .net_receive = for (o.message_buffer, 0..) |*msg, msg_i| { const remaining_data_buffer = o.data_buffer[data_i..]; - netReceivePosix(o.socket_handle, msg, remaining_data_buffer, o.flags, true) catch |err| switch (err) { + const remaining_control_buffer = o.control_buffer[control_i..]; + netReceiveOnePosix(o.socket_handle, msg, remaining_data_buffer, remaining_control_buffer, o.flags, true) catch |err| switch (err) { error.Canceled => |e| return e, error.WouldBlock => { if (msg_i != 0) break .{ null, msg_i }; @@ -2837,6 +2839,7 @@ fn batchAwaitConcurrent(userdata: ?*anyopaque, b: *Io.Batch, timeout: Io.Timeout else => |e| break .{ e, 0 }, }; data_i += msg.data.len; + control_i += msg.control.len; } else .{ null, o.message_buffer.len } }; switch (b.completed.tail) { .none => b.completed.head = index, @@ -3279,7 +3282,7 @@ fn batchDrainSubmittedWindows(t: *Threaded, b: *Io.Batch, concurrency: bool) (Io // TODO integrate with overlapped I/O or equivalent to avoid this error if (concurrency) return error.ConcurrencyUnavailable; batchCompleteBlockingWindows(b, operation_userdata, .{ - .net_receive = netReceiveWindows(t, o.socket_handle, o.message_buffer, o.data_buffer, o.flags), + .net_receive = netReceiveWindows(t, o.socket_handle, o.message_buffer, o.data_buffer, o.control_buffer, o.flags), }); }, .net_send => |*o| { @@ -13212,10 +13215,11 @@ fn netSendManyPosix( } } -fn netReceivePosix( +fn netReceiveOnePosix( socket_handle: net.Socket.Handle, message: *net.IncomingMessage, data_buffer: []u8, + control_buffer: []u8, flags: net.ReceiveFlags, nonblocking: bool, ) (net.Socket.ReceiveError || error{WouldBlock})!void { @@ -13238,22 +13242,24 @@ fn netReceivePosix( .namelen = @sizeOf(PosixAddress), .iov = (&iov)[0..1], .iovlen = 1, - .control = message.control.ptr, - .controllen = @intCast(message.control.len), + .control = control_buffer.ptr, + .controllen = @intCast(control_buffer.len), .flags = 0, }; const syscall = try Syscall.start(); while (true) { const rc = posix.system.recvmsg(socket_handle, &msg, posix_flags); + const rc_control = msg.controllen; switch (posix.errno(rc)) { .SUCCESS => { syscall.finish(); const data = data_buffer[0..@intCast(rc)]; + const control = control_buffer[0..@intCast(rc_control)]; message.* = .{ .from = addressFromPosix(&storage), .data = data, - .control = if (msg.control) |ptr| @as([*]u8, @ptrCast(ptr))[0..msg.controllen] else message.control, + .control = control, .flags = .{ .eor = (msg.flags & posix.MSG.EOR) != 0, .trunc = (msg.flags & posix.MSG.TRUNC) != 0, @@ -13294,8 +13300,12 @@ fn netReceiveWindows( socket_handle: net.Socket.Handle, message_buffer: []net.IncomingMessage, data_buffer: []u8, + control_buffer: []u8, flags: net.ReceiveFlags, ) struct { ?net.Socket.ReceiveError, usize } { + // Windows doesn't have the concept of control/ancillary data. + _ = control_buffer; + t.netReceiveOneWindows(socket_handle, &message_buffer[0], data_buffer, flags) catch |err| return .{ err, 0 }; return .{ null, 1 }; diff --git a/lib/std/Io/Uring.zig b/lib/std/Io/Uring.zig index 907d96c6fc7d12264d6a1615ff1f46133dc5914a..8e0e0c138479d04785de62bd03e23a7f4c908ffc 100644 --- a/lib/std/Io/Uring.zig +++ b/lib/std/Io/Uring.zig @@ -2095,7 +2095,7 @@ fn operate(userdata: ?*anyopaque, operation: Io.Operation) Io.Cancelable!Io.Oper }, .net_receive => |o| .{ .net_receive = r: { - const opt_err, const n = ev.netReceive(&maybe_sync.cancel_region, o.socket_handle, o.message_buffer, o.data_buffer, o.flags); + const opt_err, const n = ev.netReceive(&maybe_sync.cancel_region, o.socket_handle, o.message_buffer, o.data_buffer, o.control_buffer, o.flags); break :r .{ if (opt_err) |err| switch (err) { error.Canceled => |e| return e, @@ -5075,15 +5075,18 @@ fn netReceive( handle: net.Socket.Handle, message_buffer: []net.IncomingMessage, data_buffer: []u8, + control_buffer: []u8, flags: net.ReceiveFlags, ) struct { ?net.Socket.ReceiveError, usize } { var message_i: usize = 0; var data_i: usize = 0; + var control_i: usize = 0; while (true) { if (message_buffer.len - message_i == 0) return .{ null, message_i }; const message = &message_buffer[message_i]; const remaining_data_buffer = data_buffer[data_i..]; + const remaining_control_buffer = control_buffer[control_i..]; var storage: PosixAddress = undefined; var iov: iovec = .{ .base = remaining_data_buffer.ptr, .len = remaining_data_buffer.len }; var msg: linux.msghdr = .{ @@ -5091,8 +5094,8 @@ fn netReceive( .namelen = @sizeOf(PosixAddress), .iov = (&iov)[0..1], .iovlen = 1, - .control = message.control.ptr, - .controllen = @intCast(message.control.len), + .control = remaining_control_buffer.ptr, + .controllen = @intCast(remaining_control_buffer.len), .flags = undefined, }; @@ -5121,11 +5124,13 @@ fn netReceive( switch (completion.errno()) { .SUCCESS => { const data = remaining_data_buffer[0..@intCast(completion.result)]; + const control = remaining_control_buffer[0..@intCast(msg.controllen)]; data_i += data.len; + control_i += @intCast(msg.controllen); message.* = .{ .from = addressFromPosix(&storage), .data = data, - .control = if (msg.control) |ptr| @as([*]u8, @ptrCast(ptr))[0..msg.controllen] else message.control, + .control = control, .flags = .{ .eor = msg.flags & linux.MSG.EOR != 0, .trunc = msg.flags & linux.MSG.TRUNC != 0, diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig index ebaab6de90cc3ef610e0245fe952a90011d6c9f1..a7e726d8a3b718f0f58729e6c194f05c7045681a 100644 --- a/lib/std/Io/net.zig +++ b/lib/std/Io/net.zig @@ -924,22 +924,20 @@ pub const ReceiveFlags = packed struct(u8) { _: u5 = 0, }; +/// Fields are populated by receive functions. pub const IncomingMessage = struct { - /// Populated by receive functions. from: IpAddress, - /// Populated by receive functions, points into the caller-supplied buffer. + /// Points into caller-supplied buffer. data: []u8, - /// Supplied by caller before calling receive functions; mutated by receive - /// functions. + /// Points into caller-supplied buffer. control: []u8, - /// Populated by receive functions. flags: Flags, - /// Useful for initializing before calling `receiveManyTimeout`. + /// Deprecated. Initialize with `undefined`. pub const init: IncomingMessage = .{ .from = undefined, .data = undefined, - .control = &.{}, + .control = undefined, .flags = undefined, }; @@ -1162,13 +1160,30 @@ pub const Socket = struct { /// Waits for data. Connectionless. /// /// See also: + /// * `receiveWithControl` /// * `receiveTimeout` + /// * `receiveWithControlTimeout` + /// * `receiveManyTimeout` + /// * `receiveManyWithControlTimeout` pub fn receive(s: *const Socket, io: Io, buffer: []u8) ReceiveError!IncomingMessage { - var message: IncomingMessage = .init; + return s.receiveWithControl(io, buffer, &.{}); + } + + /// Waits for data, allowing the reception of control data. Connectionless. + /// + /// See also: + /// * `receive` + /// * `receiveTimeout` + /// * `receiveWithControlTimeout` + /// * `receiveManyTimeout` + /// * `receiveManyWithControlTimeout` + pub fn receiveWithControl(s: *const Socket, io: Io, buffer: []u8, control: []u8) ReceiveError!IncomingMessage { + var message: IncomingMessage = undefined; const maybe_err, const count = (try io.operate(.{ .net_receive = .{ .socket_handle = s.handle, .message_buffer = (&message)[0..1], .data_buffer = buffer, + .control_buffer = control, .flags = .{}, } })).net_receive; if (maybe_err) |err| return err; @@ -1184,18 +1199,42 @@ pub const Socket = struct { /// /// See also: /// * `receive` + /// * `receiveWithControl` + /// * `receiveWithControlTimeout` /// * `receiveManyTimeout` + /// * `receiveManyWithControlTimeout` pub fn receiveTimeout( s: *const Socket, io: Io, buffer: []u8, timeout: Io.Timeout, ) ReceiveTimeoutError!IncomingMessage { - var message: IncomingMessage = .init; + return s.receiveWithControlTimeout(io, buffer, &.{}, timeout); + } + + /// Waits for data, allowing the reception of control data. Connectionless. + /// + /// Returns `error.Timeout` if no message arrives early enough. + /// + /// See also: + /// * `receive` + /// * `receiveWithControl` + /// * `receiveTimeout` + /// * `receiveManyTimeout` + /// * `receiveManyWithControlTimeout` + pub fn receiveWithControlTimeout( + s: *const Socket, + io: Io, + buffer: []u8, + control: []u8, + timeout: Io.Timeout, + ) ReceiveTimeoutError!IncomingMessage { + var message: IncomingMessage = undefined; const maybe_err, const count = (try io.operateTimeout(.{ .net_receive = .{ .socket_handle = s.handle, .message_buffer = (&message)[0..1], .data_buffer = buffer, + .control_buffer = control, .flags = .{}, } }, timeout)).net_receive; if (maybe_err) |err| return err; @@ -1211,21 +1250,47 @@ pub const Socket = struct { /// /// See also: /// * `receive` + /// * `receiveWithControl` /// * `receiveTimeout` + /// * `receiveWithControlTimeout` + /// * `receiveManyWithControlTimeout` pub fn receiveManyTimeout( s: *const Socket, io: Io, - /// Function assumes each element has initialized `control` field. - /// Initializing with `IncomingMessage.init` may be helpful. message_buffer: []IncomingMessage, data_buffer: []u8, flags: ReceiveFlags, timeout: Io.Timeout, + ) struct { ?ReceiveTimeoutError, usize } { + return s.receiveManyWithControlTimeout(io, message_buffer, data_buffer, &.{}, flags, timeout); + } + + /// Waits until at least one message is delivered, possibly returning more + /// than one message. Allows reception of control data. Connectionless. + /// + /// Returns number of messages received, or `error.Timeout` if no message + /// arrives early enough. + /// + /// See also: + /// * `receive` + /// * `receiveWithControl` + /// * `receiveTimeout` + /// * `receiveWithControlTimeout` + /// * `receiveManyTimeout` + pub fn receiveManyWithControlTimeout( + s: *const Socket, + io: Io, + message_buffer: []IncomingMessage, + data_buffer: []u8, + control_buffer: []u8, + flags: ReceiveFlags, + timeout: Io.Timeout, ) struct { ?ReceiveTimeoutError, usize } { const result = io.operateTimeout(.{ .net_receive = .{ .socket_handle = s.handle, .message_buffer = message_buffer, .data_buffer = data_buffer, + .control_buffer = control_buffer, .flags = flags, } }, timeout) catch |err| return .{ err, 0 }; return result.net_receive; -- 2.54.0 From 875cb2ff2f321ec40b02f8de40230a28e56d6844 Mon Sep 17 00:00:00 2001 From: hemisputnik Date: Wed, 22 Jul 2026 11:18:38 +0300 Subject: [PATCH 04/10] std.Io.net: read control data with .net_read Also adds a way to read control data using Stream.Reader. Stream.Reader will buffer control data until it can't no more, in which case it will be truncated. The readv syscall in netReadPosix has been replaced with recvmsg. --- lib/std/Io.zig | 3 +- lib/std/Io/Threaded.zig | 34 ++++++++++++++++----- lib/std/Io/net.zig | 65 +++++++++++++++++++++++++++++++++++++---- 3 files changed, 87 insertions(+), 15 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 7d43bef3bdd52f6e4bcbc17351b1e2c9107f25f0..e7bef51fce5b8160af6b5d1deef4869edd6e594a 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -431,6 +431,7 @@ pub const Operation = union(enum) { pub const NetRead = struct { socket_handle: net.Socket.Handle, data: [][]u8, + control: []u8 = &.{}, pub const Error = error{ SystemResources, @@ -444,7 +445,7 @@ pub const Operation = union(enum) { ConnectionTimedOut, } || Io.UnexpectedError; - pub const Result = Error!usize; + pub const Result = Error!net.Stream.ReadResult; }; pub const NetWrite = struct { diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 33f9083bc54896d1c1741293eb38eb28033e8046..4ae0a35668728c0a17b1708d6cba113dfe843e60 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -2583,7 +2583,7 @@ fn operate(userdata: ?*anyopaque, operation: Io.Operation) Io.Cancelable!Io.Oper }, }, .net_read => |o| return .{ - .net_read = netRead(o.socket_handle, o.data) catch |err| switch (err) { + .net_read = netRead(o.socket_handle, o.data, o.control) catch |err| switch (err) { error.Canceled => |e| return e, else => |e| e, }, @@ -3296,7 +3296,7 @@ fn batchDrainSubmittedWindows(t: *Threaded, b: *Io.Batch, concurrency: bool) (Io // TODO integrate with overlapped I/O or equivalent to avoid this error if (concurrency) return error.ConcurrencyUnavailable; batchCompleteBlockingWindows(b, operation_userdata, .{ - .net_read = netRead(o.socket_handle, o.data) catch |err| switch (err) { + .net_read = netRead(o.socket_handle, o.data, o.control) catch |err| switch (err) { error.Canceled => |e| return e, else => |e| e, }, @@ -12886,14 +12886,14 @@ fn deferAcceptAfd(t: *Threaded, listen_handle: net.Socket.Handle, info: windows. } } -fn netRead(socket_handle: net.Socket.Handle, data: [][]u8) net.Stream.Reader.Error!usize { +fn netRead(socket_handle: net.Socket.Handle, data: [][]u8, control: []u8) net.Stream.Reader.Error!net.Stream.ReadResult { if (!have_networking) return error.NetworkDown; - if (is_windows) return netReadWindows(socket_handle, data); - return netReadPosix(socket_handle, data); + if (is_windows) return .{ .data_len = try netReadWindows(socket_handle, data) }; + return netReadPosix(socket_handle, data, control); } -fn netReadPosix(fd: net.Socket.Handle, data: [][]u8) net.Stream.Reader.Error!usize { +fn netReadPosix(fd: net.Socket.Handle, data: [][]u8, control: []u8) net.Stream.Reader.Error!net.Stream.ReadResult { var iovecs_buffer: [max_iovecs_len]posix.iovec = undefined; var i: usize = 0; for (data) |buf| { @@ -12939,13 +12939,31 @@ fn netReadPosix(fd: net.Socket.Handle, data: [][]u8) net.Stream.Reader.Error!usi } } + var msg: posix.msghdr = .{ + .name = null, + .namelen = 0, + .iov = dest.ptr, + .iovlen = @intCast(dest.len), + .control = control.ptr, + .controllen = @intCast(control.len), + .flags = 0, + }; + + const flags: u32 = + @as(u32, if (@hasDecl(posix.MSG, "CMSG_CLOEXEC")) posix.MSG.CMSG_CLOEXEC else 0); + const syscall: Syscall = try .start(); while (true) { - const rc = posix.system.readv(fd, dest.ptr, @intCast(dest.len)); + const rc = posix.system.recvmsg(fd, &msg, flags); + const rc_control = msg.controllen; switch (posix.errno(rc)) { .SUCCESS => { syscall.finish(); - return @intCast(rc); + return .{ + .data_len = @intCast(rc), + .control_len = @intCast(rc_control), + .control_truncated = (msg.flags & posix.MSG.CTRUNC) != 0, + }; }, .INTR => { try syscall.checkCancel(); diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig index a7e726d8a3b718f0f58729e6c194f05c7045681a..82bbcc0e707836d807cc5f7dea739f0668a583b5 100644 --- a/lib/std/Io/net.zig +++ b/lib/std/Io/net.zig @@ -1334,13 +1334,37 @@ pub const Stream = struct { const max_iovecs_len = 8; + pub const ReadResult = struct { + data_len: usize, + control_len: usize = 0, + /// Whether only some of the control data was received. + /// + /// When control data is truncated, the extra data essentially + /// disappears into the ether. This is indicative of a design problem, + /// such as the control buffer being too small. + control_truncated: bool = false, + }; + /// This is a low-level API that calls the `Io` interface function directly. /// For a higher level API, see `reader`. pub fn read(s: *const Stream, io: Io, data: [][]u8) Reader.Error!usize { - return (try io.operate(.{ .net_read = .{ + const rc, _ = try (try io.operate(.{ .net_read = .{ .socket_handle = s.socket.handle, .data = data, } })).net_read; + return rc; + } + + /// Read with control data. + /// + /// This is a low-level API that calls the `Io` interface function directly. + /// For a higher level API, see `reader`. + pub fn readWithControl(s: *const Stream, io: Io, data: [][]u8, control: []u8) Reader.Error!ReadResult { + return try (try io.operate(.{ .net_read = .{ + .socket_handle = s.socket.handle, + .data = data, + .control = control, + } })).net_read; } pub fn close(s: *const Stream, io: Io) void { @@ -1354,12 +1378,20 @@ pub const Stream = struct { pub const Reader = struct { io: Io, interface: Io.Reader, + control_buffer: []u8, + control_len: usize, + control_truncated: bool, stream: Stream, err: ?Error, pub const Error = Io.Operation.NetRead.Error || Io.Cancelable; pub fn init(stream: Stream, io: Io, buffer: []u8) Reader { + return initWithControl(stream, io, buffer, &.{}); + } + + /// Same as `init`, but also provides a buffer for storing control data. + pub fn initWithControl(stream: Stream, io: Io, buffer: []u8, control_buffer: []u8) Reader { return .{ .io = io, .interface = .{ @@ -1371,6 +1403,9 @@ pub const Stream = struct { .seek = 0, .end = 0, }, + .control_buffer = control_buffer, + .control_len = 0, + .control_truncated = false, .stream = stream, .err = null, }; @@ -1391,18 +1426,31 @@ pub const Stream = struct { const dest_n, const data_size = try io_r.writableVector(&iovecs_buffer, data); const dest = iovecs_buffer[0..dest_n]; assert(dest[0].len > 0); - const n = r.stream.read(io, dest) catch |err| { + const result = r.stream.readWithControl(io, dest, r.control_buffer[r.control_len..]) catch |err| { r.err = err; return error.ReadFailed; }; - if (n == 0) { + r.control_len += result.control_len; + r.control_truncated = r.control_truncated or result.control_truncated; + if (result.data_len == 0) { return error.EndOfStream; } - if (n > data_size) { - r.interface.end += n - data_size; + if (result.data_len > data_size) { + r.interface.end += result.data_len - data_size; return data_size; } - return n; + return result.data_len; + } + + /// Get buffered control data. + pub fn controlSlice(r: *const Reader) []u8 { + return r.control_buffer[0..r.control_len]; + } + + /// Clear buffered control data. + pub fn clearControl(r: *Reader) void { + r.control_len = 0; + r.control_truncated = false; } }; @@ -1489,6 +1537,11 @@ pub const Stream = struct { return .init(stream, io, buffer); } + /// Same as `reader`, but also provides a buffer for storing control data. + pub fn readerWithControl(stream: Stream, io: Io, buffer: []u8, control_buffer: []u8) Reader { + return .initWithControl(stream, io, buffer, control_buffer); + } + pub fn writer(stream: Stream, io: Io, buffer: []u8) Writer { return .init(stream, io, buffer); } -- 2.54.0 From c4d9d65a7bfb0e675ff9b43c50332032bcec6b25 Mon Sep 17 00:00:00 2001 From: hemisputnik Date: Wed, 22 Jul 2026 11:28:31 +0300 Subject: [PATCH 05/10] std.Io.net: send control data with .net_write Add a new parameter to .net_write - control. --- lib/std/Io.zig | 1 + lib/std/Io/Threaded.zig | 15 ++++++++++----- lib/std/Io/net.zig | 4 ++++ 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index e7bef51fce5b8160af6b5d1deef4869edd6e594a..ab0f8514b407cb567c048db502eb3b52d4a27fd4 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -453,6 +453,7 @@ pub const Operation = union(enum) { header: []const u8 = &.{}, data: []const []const u8, splat: usize = 1, + control: []const u8 = &.{}, pub const Error = error{ /// Another TCP Fast Open is already in progress. diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 4ae0a35668728c0a17b1708d6cba113dfe843e60..77f14f6f991faa3ec821c8ad784445f1c2dd8d09 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -2590,9 +2590,9 @@ fn operate(userdata: ?*anyopaque, operation: Io.Operation) Io.Cancelable!Io.Oper }, .net_write => |o| return .{ .net_write = (if (is_windows) - netWriteWindows(o.socket_handle, o.header, o.data, o.splat) + netWriteWindows(o.socket_handle, o.header, o.data, o.splat, o.control) else - netWritePosix(o.socket_handle, o.header, o.data, o.splat)) catch |err| switch (err) { + netWritePosix(o.socket_handle, o.header, o.data, o.splat, o.control)) catch |err| switch (err) { error.Canceled => |e| return e, else => |e| e, }, @@ -3306,7 +3306,7 @@ fn batchDrainSubmittedWindows(t: *Threaded, b: *Io.Batch, concurrency: bool) (Io // TODO integrate with overlapped I/O or equivalent to avoid this error if (concurrency) return error.ConcurrencyUnavailable; batchCompleteBlockingWindows(b, operation_userdata, .{ - .net_write = netWriteWindows(o.socket_handle, o.header, o.data, o.splat) catch |err| switch (err) { + .net_write = netWriteWindows(o.socket_handle, o.header, o.data, o.splat, o.control) catch |err| switch (err) { error.Canceled => |e| return e, else => |e| e, }, @@ -13389,6 +13389,7 @@ fn netWritePosix( header: []const u8, data: []const []const u8, splat: usize, + control: []const u8, ) net.Stream.Writer.Error!usize { if (!have_networking) return error.NetworkDown; @@ -13398,8 +13399,8 @@ fn netWritePosix( .namelen = 0, .iov = &iovecs, .iovlen = 0, - .control = null, - .controllen = 0, + .control = if (control.len == 0) null else @constCast(control.ptr), + .controllen = @intCast(control.len), .flags = 0, }; addBuf(&iovecs, &msg.iovlen, header); @@ -13481,9 +13482,13 @@ fn netWriteWindows( header: []const u8, data: []const []const u8, splat: usize, + control: []const u8, ) net.Stream.Writer.Error!usize { if (!have_networking) return error.NetworkDown; + // Windows doesn't have the concept of control/ancillary data. + _ = control; + var iovecs: [max_iovecs_len]windows.AFD.WSABUF(.@"const") = undefined; var len: u32 = 0; addAfdBuf(.@"const", &iovecs, &len, header); diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig index 82bbcc0e707836d807cc5f7dea739f0668a583b5..01547a1a9df1c53ffdac6a370f2108136d6b6af2 100644 --- a/lib/std/Io/net.zig +++ b/lib/std/Io/net.zig @@ -1457,6 +1457,8 @@ pub const Stream = struct { pub const Writer = struct { io: Io, interface: Io.Writer, + /// Will be sent on next drain, then set back to an empty slice. + control: []const u8 = &.{}, stream: Stream, err: ?Error = null, write_file_err: ?WriteFileError = null, @@ -1499,6 +1501,7 @@ pub const Stream = struct { .header = buffered, .data = data, .splat = splat, + .control = w.control, } }) catch |err| { w.err = err; return error.WriteFailed; @@ -1507,6 +1510,7 @@ pub const Stream = struct { w.err = err; return error.WriteFailed; }; + w.control = &.{}; return io_w.consume(n); } -- 2.54.0 From faa0a4a54ec9ff86100e306457b631e3cb23773d Mon Sep 17 00:00:00 2001 From: hemisputnik Date: Wed, 22 Jul 2026 11:44:29 +0300 Subject: [PATCH 06/10] std.posix: add cmsghdr --- lib/std/posix.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/std/posix.zig b/lib/std/posix.zig index 712b093e71f458810d4a8e11e451c7a44f9905d1..ab621d54d7f6a059b307b75c541b28d03e289e8f 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -150,6 +150,7 @@ pub const blkcnt_t = system.blkcnt_t; pub const blksize_t = system.blksize_t; pub const clock_t = system.clock_t; pub const clockid_t = system.clockid_t; +pub const cmsghdr = system.cmsghdr; pub const timerfd_clockid_t = system.timerfd_clockid_t; pub const cpu_set_t = system.cpu_set_t; pub const dev_t = system.dev_t; -- 2.54.0 From b3e8eb657772e46058ab011eb875b43d181e56c0 Mon Sep 17 00:00:00 2001 From: hemisputnik Date: Thu, 23 Jul 2026 03:08:48 +0300 Subject: [PATCH 07/10] std.Io.net.test: add fd passing test Similarly to the other unix socket test, this test opens a unix socket, but also uses the newly-introduced control data API to pass a file descriptor to the other side. The other side then writes to the file descriptor, then deletes the temporary file. --- lib/std/Io/net/test.zig | 114 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/lib/std/Io/net/test.zig b/lib/std/Io/net/test.zig index 45389715cd749e77fe4ff99b088b92b78968e262..0edd1fbee6e0a30b9ae38904daf6a74e8a697062 100644 --- a/lib/std/Io/net/test.zig +++ b/lib/std/Io/net/test.zig @@ -295,6 +295,120 @@ test "listen on a unix socket, send bytes, receive bytes" { try client_task.await(io); } +test "listen on a unix socket, pass file descriptor" { + if (builtin.os.tag == .windows or builtin.os.tag == .wasi) { + // Windows and WASI don't have the concept of control/ancillary data. + return error.SkipZigTest; + } + + const io = testing.io; + const gpa = testing.allocator; + + const socket_path = try generateFileName(gpa, io, "socket.unix"); + defer gpa.free(socket_path); + const temp_file_path = try generateFileName(gpa, io, "temp_file"); + defer gpa.free(temp_file_path); + + defer Io.Dir.cwd().deleteFile(io, socket_path) catch {}; + defer Io.Dir.cwd().deleteFile(io, temp_file_path) catch {}; + + const socket_addr = try net.UnixAddress.init(socket_path); + + var server = socket_addr.listen(io, .{}) catch |err| switch (err) { + error.AddressFamilyUnsupported => return error.SkipZigTest, + error.NetworkDown => return error.SkipZigTest, + else => |e| return e, + }; + defer server.socket.close(io); + + const cmsg_alignment = switch (builtin.os.tag) { + .driverkit, + .ios, + .maccatalyst, + .macos, + .tvos, + .visionos, + .watchos, + => 4, + else => @sizeOf(usize), + }; + + const cmsghdr_aligned_len = comptime mem.alignForward(usize, @sizeOf(std.posix.cmsghdr), cmsg_alignment); + const cmsghdr_align = @alignOf(std.posix.cmsghdr); + const fd_t_len = @sizeOf(std.posix.fd_t); + + const cmsg_buf_len = + cmsghdr_aligned_len + + comptime mem.alignForward(usize, fd_t_len, cmsg_alignment); + + const S = struct { + fn clientFn(path: []const u8, file_path: []const u8) !void { + const temp_file = try Io.Dir.cwd().createFile(io, file_path, .{}); + defer temp_file.close(io); + + const server_path: net.UnixAddress = try .init(path); + var stream = try server_path.connect(io); + defer stream.close(io); + + var cmsg_buf: [cmsg_buf_len]u8 align(cmsghdr_align) = @splat(0); + + const header_ptr: *std.posix.cmsghdr = @ptrCast(&cmsg_buf); + header_ptr.* = .{ + .len = @intCast(cmsghdr_aligned_len + fd_t_len), + .level = std.posix.SOL.SOCKET, + .type = std.posix.SCM.RIGHTS, + }; + + const fds = mem.bytesAsSlice(std.posix.fd_t, cmsg_buf[cmsghdr_aligned_len..]); + fds[0] = temp_file.handle; + + var stream_writer = stream.writer(io, &.{}); + stream_writer.control = &cmsg_buf; + try stream_writer.interface.writeAll("Hello world!"); + } + }; + + var client_task = io.concurrent(S.clientFn, .{ socket_path, temp_file_path }) catch |err| switch (err) { + error.ConcurrencyUnavailable => return error.SkipZigTest, + }; + defer client_task.cancel(io) catch {}; + + var stream = try server.accept(io); + defer stream.close(io); + + var control_buf: [32]u8 align(cmsghdr_align) = undefined; + var buf: [16]u8 = undefined; + var stream_reader = stream.readerWithControl(io, &.{}, &control_buf); + + const n = try stream_reader.interface.readSliceShort(&buf); + const cmsg_buf = stream_reader.controlSlice(); + + try testing.expect(!stream_reader.control_truncated); // The control buffer should be big enough to receive the entire control message + + try testing.expectEqual(12, n); + try testing.expectEqualStrings("Hello world!", buf[0..n]); + + try testing.expect(cmsg_buf.len >= cmsghdr_aligned_len + fd_t_len); + + const header_ptr: *const std.posix.cmsghdr = @ptrCast(@alignCast(cmsg_buf)); + + try testing.expectEqual(cmsghdr_aligned_len + fd_t_len, header_ptr.len); + try testing.expectEqual(std.posix.SOL.SOCKET, header_ptr.level); + try testing.expectEqual(std.posix.SCM.RIGHTS, header_ptr.type); + + const fds = mem.bytesAsSlice(std.posix.fd_t, cmsg_buf[cmsghdr_aligned_len..]); + + const temp_file: Io.File = .{ + .handle = fds[0], + .flags = .{ .nonblocking = false }, + }; + defer temp_file.close(io); + var temp_file_writer = temp_file.writer(io, &.{}); + try temp_file_writer.interface.writeAll("if this is a real file descriptor, this shouldn't fail"); + + try client_task.await(io); +} + fn generateFileName(gpa: Allocator, io: Io, base_name: []const u8) ![]const u8 { const random_bytes_count = 12; const sub_path_len = comptime std.base64.url_safe.Encoder.calcSize(random_bytes_count); -- 2.54.0 From c53cb65568e80a1d9c20a07373db86377ee9421b Mon Sep 17 00:00:00 2001 From: Isaac Freund Date: Wed, 30 Sep 2026 14:36:03 +0200 Subject: [PATCH 08/10] std.Io.net: add control message utilities --- lib/std/Io/net.zig | 72 ++++++++++++++++++++++++++++++++++++++--- lib/std/Io/net/test.zig | 52 +++++++++-------------------- lib/std/c.zig | 62 +++++++++++++++++++++++++++++++++++ lib/std/os/linux.zig | 3 ++ lib/std/posix.zig | 1 + 5 files changed, 149 insertions(+), 41 deletions(-) diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig index 01547a1a9df1c53ffdac6a370f2108136d6b6af2..9fe2661af17a69649629252ea200407a0151b798 100644 --- a/lib/std/Io/net.zig +++ b/lib/std/Io/net.zig @@ -1378,7 +1378,7 @@ pub const Stream = struct { pub const Reader = struct { io: Io, interface: Io.Reader, - control_buffer: []u8, + control_buffer: []align(cmsg_align) u8, control_len: usize, control_truncated: bool, stream: Stream, @@ -1391,7 +1391,12 @@ pub const Stream = struct { } /// Same as `init`, but also provides a buffer for storing control data. - pub fn initWithControl(stream: Stream, io: Io, buffer: []u8, control_buffer: []u8) Reader { + pub fn initWithControl( + stream: Stream, + io: Io, + buffer: []u8, + control_buffer: []align(cmsg_align) u8, + ) Reader { return .{ .io = io, .interface = .{ @@ -1442,11 +1447,16 @@ pub const Stream = struct { return result.data_len; } - /// Get buffered control data. - pub fn controlSlice(r: *const Reader) []u8 { + /// Access raw buffered control data. + pub fn controlSlice(r: *const Reader) []align(cmsg_align) u8 { return r.control_buffer[0..r.control_len]; } + /// Iterate over buffered control messages. + pub fn controlIterator(r: *const Reader) cmsg.Iterator { + return .{ .control = r.controlSlice() }; + } + /// Clear buffered control data. pub fn clearControl(r: *Reader) void { r.control_len = 0; @@ -1542,7 +1552,12 @@ pub const Stream = struct { } /// Same as `reader`, but also provides a buffer for storing control data. - pub fn readerWithControl(stream: Stream, io: Io, buffer: []u8, control_buffer: []u8) Reader { + pub fn readerWithControl( + stream: Stream, + io: Io, + buffer: []u8, + control_buffer: []align(cmsg_align) u8, + ) Reader { return .initWithControl(stream, io, buffer, control_buffer); } @@ -1551,6 +1566,53 @@ pub const Stream = struct { } }; +pub const cmsg_align = if (@TypeOf(std.posix.cmsg_align) == void) 1 else std.posix.cmsg_align; +/// Utility Functions for interacting with POSIX socket control messages. +/// See also https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/sys_socket.h.html +pub const cmsg = struct { + const cmsghdr = std.posix.cmsghdr; + + /// Equivalent to `CMSG_SPACE` in C. + pub fn space(data_len: usize) usize { + return std.mem.alignForward(usize, @sizeOf(cmsghdr), cmsg_align) + + std.mem.alignForward(usize, data_len, cmsg_align); + } + + /// Equivalent to `CMSG_LEN` in C. + pub fn len(data_len: @FieldType(cmsghdr, "len")) @TypeOf(data_len) { + return std.mem.alignForward(@TypeOf(data_len), @sizeOf(cmsghdr), cmsg_align) + data_len; + } + + /// Equivalent to `CMSG_DATA` in C. + pub fn data(header: *align(cmsg_align) cmsghdr) []align(cmsg_align) u8 { + const bytes: [*]u8 = @ptrCast(header); + return @alignCast(bytes[0..header.len][std.mem.alignForward(usize, @sizeOf(cmsghdr), cmsg_align)..]); + } + + /// Equivalent to `CMSG_FIRSTHDR` and `CMSG_NXTHDR` in C. + pub const Iterator = struct { + control: []align(cmsg_align) u8, + + pub const Message = struct { + header: *cmsghdr, + data: []align(cmsg_align) u8, + }; + + pub fn next(it: *Iterator) ?Message { + if (it.control.len < @sizeOf(cmsghdr)) return null; + const header: *align(cmsg_align) cmsghdr = @ptrCast(it.control.ptr); + if (it.control.len < header.len) return null; + const next_header = std.mem.alignForward(usize, header.len, cmsg_align); + if (it.control.len < next_header) { + it.control = &.{}; + } else { + it.control = @alignCast(it.control[next_header..]); + } + return .{ .header = header, .data = data(header) }; + } + }; +}; + pub const Server = struct { socket: Socket, options: AcceptOptions, diff --git a/lib/std/Io/net/test.zig b/lib/std/Io/net/test.zig index 0edd1fbee6e0a30b9ae38904daf6a74e8a697062..2c5a4a852e201d6480b1e4e790560c787d881602 100644 --- a/lib/std/Io/net/test.zig +++ b/lib/std/Io/net/test.zig @@ -4,6 +4,7 @@ const std = @import("std"); const Io = std.Io; const net = std.Io.net; const mem = std.mem; +const posix = std.posix; const testing = std.testing; const Allocator = std.mem.Allocator; @@ -321,26 +322,6 @@ test "listen on a unix socket, pass file descriptor" { }; defer server.socket.close(io); - const cmsg_alignment = switch (builtin.os.tag) { - .driverkit, - .ios, - .maccatalyst, - .macos, - .tvos, - .visionos, - .watchos, - => 4, - else => @sizeOf(usize), - }; - - const cmsghdr_aligned_len = comptime mem.alignForward(usize, @sizeOf(std.posix.cmsghdr), cmsg_alignment); - const cmsghdr_align = @alignOf(std.posix.cmsghdr); - const fd_t_len = @sizeOf(std.posix.fd_t); - - const cmsg_buf_len = - cmsghdr_aligned_len + - comptime mem.alignForward(usize, fd_t_len, cmsg_alignment); - const S = struct { fn clientFn(path: []const u8, file_path: []const u8) !void { const temp_file = try Io.Dir.cwd().createFile(io, file_path, .{}); @@ -350,16 +331,16 @@ test "listen on a unix socket, pass file descriptor" { var stream = try server_path.connect(io); defer stream.close(io); - var cmsg_buf: [cmsg_buf_len]u8 align(cmsghdr_align) = @splat(0); + var cmsg_buf: [net.cmsg.space(@sizeOf(posix.fd_t))]u8 align(Io.net.cmsg_align) = @splat(0); - const header_ptr: *std.posix.cmsghdr = @ptrCast(&cmsg_buf); - header_ptr.* = .{ - .len = @intCast(cmsghdr_aligned_len + fd_t_len), - .level = std.posix.SOL.SOCKET, - .type = std.posix.SCM.RIGHTS, + const header: *align(Io.net.cmsg_align) posix.cmsghdr = @ptrCast(&cmsg_buf); + header.* = .{ + .len = net.cmsg.len(@sizeOf(posix.fd_t)), + .level = posix.SOL.SOCKET, + .type = posix.SCM.RIGHTS, }; - const fds = mem.bytesAsSlice(std.posix.fd_t, cmsg_buf[cmsghdr_aligned_len..]); + const fds: []posix.fd_t = @ptrCast(net.cmsg.data(header)); fds[0] = temp_file.handle; var stream_writer = stream.writer(io, &.{}); @@ -376,27 +357,26 @@ test "listen on a unix socket, pass file descriptor" { var stream = try server.accept(io); defer stream.close(io); - var control_buf: [32]u8 align(cmsghdr_align) = undefined; + var control_buf: [32]u8 align(Io.net.cmsg_align) = undefined; var buf: [16]u8 = undefined; var stream_reader = stream.readerWithControl(io, &.{}, &control_buf); const n = try stream_reader.interface.readSliceShort(&buf); - const cmsg_buf = stream_reader.controlSlice(); try testing.expect(!stream_reader.control_truncated); // The control buffer should be big enough to receive the entire control message try testing.expectEqual(12, n); try testing.expectEqualStrings("Hello world!", buf[0..n]); - try testing.expect(cmsg_buf.len >= cmsghdr_aligned_len + fd_t_len); + var it = stream_reader.controlIterator(); + const control = it.next() orelse return error.TestUnexpectedResult; + try testing.expectEqual(null, it.next()); - const header_ptr: *const std.posix.cmsghdr = @ptrCast(@alignCast(cmsg_buf)); + try testing.expectEqual(net.cmsg.len(@sizeOf(posix.fd_t)), control.header.len); + try testing.expectEqual(posix.SOL.SOCKET, control.header.level); + try testing.expectEqual(posix.SCM.RIGHTS, control.header.type); - try testing.expectEqual(cmsghdr_aligned_len + fd_t_len, header_ptr.len); - try testing.expectEqual(std.posix.SOL.SOCKET, header_ptr.level); - try testing.expectEqual(std.posix.SCM.RIGHTS, header_ptr.type); - - const fds = mem.bytesAsSlice(std.posix.fd_t, cmsg_buf[cmsghdr_aligned_len..]); + const fds: []posix.fd_t = @ptrCast(control.data); const temp_file: Io.File = .{ .handle = fds[0], diff --git a/lib/std/c.zig b/lib/std/c.zig index 51c6656d4f21dc23717f757faddc76f109049e2a..1ce75c984c1ac06382bfe2ec79303d0ec26ae128 100644 --- a/lib/std/c.zig +++ b/lib/std/c.zig @@ -4432,6 +4432,68 @@ const posix_cmsghdr = extern struct { type: c_int, }; +// Alignment used by POSIX specified CMSG_* Macros +// https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/sys_socket.h.html +pub const cmsg_align = switch (native_os) { + .linux => linux.cmsg_align, + // https://github.com/emscripten-core/emscripten/blob/96371ed7888fc78c040179f4d4faa82a6a07a116/system/lib/libc/musl/include/sys/socket.h#L362 + .emscripten => @sizeOf(usize), + // https://github.com/freebsd/freebsd-src/blob/b197d2abcb6895d78bc9df8404e374397aa44748/sys/sys/socket.h#L584 + // https://github.com/freebsd/freebsd-src/blob/d2018cedb414ef17e77b7fc2fa19fd81328da46b/sys/sys/_align.h#L30 + .freebsd => @sizeOf(*anyopaque), + // https://github.com/DragonFlyBSD/DragonFlyBSD/blob/d1f4fb943c73e2b61ddabff6aa48ce7551db72f4/sys/sys/socket.h#L441 + // https://github.com/DragonFlyBSD/DragonFlyBSD/blob/d1f4fb943c73e2b61ddabff6aa48ce7551db72f4/sys/cpu/x86_64/include/alignbytes.h#L35 + .dragonfly => switch (builtin.cpu.arch) { + .x86_64 => @sizeOf(c_long), + else => {}, + }, + // https://github.com/NetBSD/src/blob/ba8e1774fd9c0c26ecca461c07bc95d9ebb69579/sys/sys/socket.h#L544 + .netbsd => switch (builtin.cpu.arch) { + // https://github.com/NetBSD/src/blob/ac016822f750dc9166c84b6d2657479992622192/sys/arch/arm/include/cdefs.h#L63 + .aarch64, .aarch64_be => @sizeOf(i128), + .arm, .armeb => @sizeOf(c_longlong), + // https://github.com/NetBSD/src/blob/ac016822f750dc9166c84b6d2657479992622192/sys/arch/i386/include/cdefs.h#L9 + .x86 => @sizeOf(c_int), + // https://github.com/NetBSD/src/blob/ac016822f750dc9166c84b6d2657479992622192/sys/arch/amd64/include/cdefs.h#L6 + .x86_64 => @sizeOf(c_long), + // https://github.com/NetBSD/src/blob/ac016822f750dc9166c84b6d2657479992622192/sys/arch/mips/include/cdefs.h#L74 + .mips, .mips64, .mipsel, .mips64el => 8, + // https://github.com/NetBSD/src/blob/ac016822f750dc9166c84b6d2657479992622192/sys/arch/sparc/include/cdefs.h#L8 + .sparc64 => 16, + // https://github.com/NetBSD/src/blob/ac016822f750dc9166c84b6d2657479992622192/sys/arch/sparc/include/cdefs.h#L10 + .sparc => 8, + // https://github.com/NetBSD/src/blob/ac016822f750dc9166c84b6d2657479992622192/sys/arch/alpha/include/cdefs.h#L6 + .alpha => 8, + // https://github.com/NetBSD/src/blob/ac016822f750dc9166c84b6d2657479992622192/sys/arch/m68k/include/cdefs.h#L6 + .m68k => @sizeOf(c_int), + // https://github.com/NetBSD/src/blob/ac016822f750dc9166c84b6d2657479992622192/sys/arch/hppa/include/cdefs.h#L12 + .hppa => 8, + // https://github.com/NetBSD/src/blob/ac016822f750dc9166c84b6d2657479992622192/sys/arch/riscv/include/cdefs.h#L5 + .riscv32, .riscv32be, .riscv64, .riscv64be => @as(comptime_int, builtin.target.cMaxIntAlignment()), + // https://github.com/NetBSD/src/blob/ac016822f750dc9166c84b6d2657479992622192/sys/arch/powerpc/include/cdefs.h#L6 + .powerpc, .powerpcle, .powerpc64, .powerpc64le => @sizeOf(f64), + // https://github.com/NetBSD/src/blob/ac016822f750dc9166c84b6d2657479992622192/sys/arch/or1k/include/cdefs.h#L6 + .or1k => @as(comptime_int, builtin.target.cMaxIntAlignment()), + else => {}, + }, + // https://github.com/openbsd/src/blob/4a7ecdb29021f26ee0ef397afdd2931e20e63071/sys/sys/socket.h#L564 + // https://github.com/openbsd/src/blob/4a7ecdb29021f26ee0ef397afdd2931e20e63071/sys/arch/amd64/include/_types.h#L50 + .openbsd => @sizeOf(c_long), + // https://github.com/illumos/illumos-gate/blob/c1e950d976948602134f0e0e5dadf3d48a7ee996/usr/src/uts/common/sys/socket.h#L469 + .illumos => 4, + // https://github.com/haiku/haiku/blob/b54f586058fd6623645512e4631468cede9933b9/headers/posix/sys/socket.h#L152 + // https://github.com/haiku/haiku/blob/6059d64cb7cd145f0f1eb5b89c383f71e4b6bebf/headers/posix/sys/param.h#L28 + .haiku => @sizeOf(c_long), + // https://github.com/apple/darwin-xnu/blob/2ff845c2e033bd0ff64b5b6aa6063a1f8f65aa32/bsd/sys/socket.h#L1077 + .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => 4, + else => {}, +}; +comptime { + if (@TypeOf(cmsg_align) != void and @TypeOf(cmsghdr) != void) { + assert(cmsg_align >= @alignOf(cmsghdr)); + } +} + pub const nfds_t = switch (native_os) { .linux => linux.nfds_t, .emscripten => emscripten.nfds_t, diff --git a/lib/std/os/linux.zig b/lib/std/os/linux.zig index be4e4fd069e736ca170cde09def4e0545f9e8e21..38e4abd197ba6c17245e7fe7038d7db0528ca395 100644 --- a/lib/std/os/linux.zig +++ b/lib/std/os/linux.zig @@ -11244,6 +11244,9 @@ pub const cmsghdr = extern struct { type: i32, }; +// https://github.com/torvalds/linux/blob/72d3fcf802c45d00b300f25b848a93c3a2bd7c7e/include/linux/socket.h#L133 +pub const cmsg_align = @sizeOf(c_long); + pub const riscv_hwprobe = extern struct { key: i64, value: u64, diff --git a/lib/std/posix.zig b/lib/std/posix.zig index ab621d54d7f6a059b307b75c541b28d03e289e8f..d845a8607dbb89550018673c8b20ec2db7aebdce 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -151,6 +151,7 @@ pub const blksize_t = system.blksize_t; pub const clock_t = system.clock_t; pub const clockid_t = system.clockid_t; pub const cmsghdr = system.cmsghdr; +pub const cmsg_align = system.cmsg_align; pub const timerfd_clockid_t = system.timerfd_clockid_t; pub const cpu_set_t = system.cpu_set_t; pub const dev_t = system.dev_t; -- 2.54.0 From 2217d43cc91145c4f569108594f6d246d4733752 Mon Sep 17 00:00:00 2001 From: Isaac Freund Date: Thu, 1 Oct 2026 10:07:01 +0200 Subject: [PATCH 09/10] Revert "std.Io.net: improve reception of control data" It is not clear to me that this is the right path to take, using a shared buffer for control data makes managing the alignment trickier, and would require the Io interface itself to deal with alignment. For an example of what I consider clean design, see net_read/net_write. There is no need for the Io interface/operations to be aware of control buffer alignment and it is sufficient for the net.Stream.Reader abstraction to use an aligned buffer. Also this has no test coverage and is (to my knowledege) not currently blocking any real world Zig project. This reverts commit 7c0347fce0b6f510b43ca503ad9c1cd2f8ac9a6b. --- lib/std/Io.zig | 1 - lib/std/Io/Kqueue.zig | 2 - lib/std/Io/Threaded.zig | 26 ++++-------- lib/std/Io/Uring.zig | 13 ++---- lib/std/Io/net.zig | 87 ++++++----------------------------------- 5 files changed, 23 insertions(+), 106 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index ab0f8514b407cb567c048db502eb3b52d4a27fd4..14c7306ca6673d99bad8f3b8b5eafc150f935f74 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -346,7 +346,6 @@ pub const Operation = union(enum) { socket_handle: net.Socket.Handle, message_buffer: []net.IncomingMessage, data_buffer: []u8, - control_buffer: []u8 = &.{}, flags: net.ReceiveFlags, pub const Error = error{ diff --git a/lib/std/Io/Kqueue.zig b/lib/std/Io/Kqueue.zig index f663ab366a157ea562809d58c4b03ef9f6861c76..8f8be91d59bd3f3131ad5c08955da9de09b90e26 100644 --- a/lib/std/Io/Kqueue.zig +++ b/lib/std/Io/Kqueue.zig @@ -1183,7 +1183,6 @@ fn netReceive( handle: net.Socket.Handle, message_buffer: []net.IncomingMessage, data_buffer: []u8, - control_buffer: []u8, flags: net.ReceiveFlags, timeout: Io.Timeout, ) struct { ?net.Socket.ReceiveTimeoutError, usize } { @@ -1192,7 +1191,6 @@ fn netReceive( _ = handle; _ = message_buffer; _ = data_buffer; - _ = control_buffer; _ = flags; _ = timeout; @panic("TODO"); diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 77f14f6f991faa3ec821c8ad784445f1c2dd8d09..2c7cdbc8e99e37081200900866b94edf7672e272 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -2555,8 +2555,8 @@ fn operate(userdata: ?*anyopaque, operation: Io.Operation) Io.Cancelable!Io.Oper .device_io_control => |*o| return .{ .device_io_control = try deviceIoControl(o) }, .net_receive => |*o| return .{ .net_receive = o: { if (!have_networking) break :o .{ error.NetworkDown, 0 }; - if (is_windows) break :o netReceiveWindows(t, o.socket_handle, o.message_buffer, o.data_buffer, o.control_buffer, o.flags); - netReceiveOnePosix(o.socket_handle, &o.message_buffer[0], o.data_buffer, o.control_buffer, o.flags, false) catch |err| switch (err) { + if (is_windows) break :o netReceiveWindows(t, o.socket_handle, o.message_buffer, o.data_buffer, o.flags); + netReceivePosix(o.socket_handle, &o.message_buffer[0], o.data_buffer, o.flags, false) catch |err| switch (err) { error.Canceled => |e| return e, error.WouldBlock => unreachable, else => |e| break :o .{ e, 0 }, @@ -2825,11 +2825,9 @@ fn batchAwaitConcurrent(userdata: ?*anyopaque, b: *Io.Batch, timeout: Io.Timeout .device_io_control => |o| try poll_storage.add(o.file.handle, posix.POLL.IN | posix.POLL.OUT | posix.POLL.ERR), .net_receive => |*o| nb: { var data_i: usize = 0; - var control_i: usize = 0; const result: Io.Operation.Result = .{ .net_receive = for (o.message_buffer, 0..) |*msg, msg_i| { const remaining_data_buffer = o.data_buffer[data_i..]; - const remaining_control_buffer = o.control_buffer[control_i..]; - netReceiveOnePosix(o.socket_handle, msg, remaining_data_buffer, remaining_control_buffer, o.flags, true) catch |err| switch (err) { + netReceivePosix(o.socket_handle, msg, remaining_data_buffer, o.flags, true) catch |err| switch (err) { error.Canceled => |e| return e, error.WouldBlock => { if (msg_i != 0) break .{ null, msg_i }; @@ -2839,7 +2837,6 @@ fn batchAwaitConcurrent(userdata: ?*anyopaque, b: *Io.Batch, timeout: Io.Timeout else => |e| break .{ e, 0 }, }; data_i += msg.data.len; - control_i += msg.control.len; } else .{ null, o.message_buffer.len } }; switch (b.completed.tail) { .none => b.completed.head = index, @@ -3282,7 +3279,7 @@ fn batchDrainSubmittedWindows(t: *Threaded, b: *Io.Batch, concurrency: bool) (Io // TODO integrate with overlapped I/O or equivalent to avoid this error if (concurrency) return error.ConcurrencyUnavailable; batchCompleteBlockingWindows(b, operation_userdata, .{ - .net_receive = netReceiveWindows(t, o.socket_handle, o.message_buffer, o.data_buffer, o.control_buffer, o.flags), + .net_receive = netReceiveWindows(t, o.socket_handle, o.message_buffer, o.data_buffer, o.flags), }); }, .net_send => |*o| { @@ -13233,11 +13230,10 @@ fn netSendManyPosix( } } -fn netReceiveOnePosix( +fn netReceivePosix( socket_handle: net.Socket.Handle, message: *net.IncomingMessage, data_buffer: []u8, - control_buffer: []u8, flags: net.ReceiveFlags, nonblocking: bool, ) (net.Socket.ReceiveError || error{WouldBlock})!void { @@ -13260,24 +13256,22 @@ fn netReceiveOnePosix( .namelen = @sizeOf(PosixAddress), .iov = (&iov)[0..1], .iovlen = 1, - .control = control_buffer.ptr, - .controllen = @intCast(control_buffer.len), + .control = message.control.ptr, + .controllen = @intCast(message.control.len), .flags = 0, }; const syscall = try Syscall.start(); while (true) { const rc = posix.system.recvmsg(socket_handle, &msg, posix_flags); - const rc_control = msg.controllen; switch (posix.errno(rc)) { .SUCCESS => { syscall.finish(); const data = data_buffer[0..@intCast(rc)]; - const control = control_buffer[0..@intCast(rc_control)]; message.* = .{ .from = addressFromPosix(&storage), .data = data, - .control = control, + .control = if (msg.control) |ptr| @as([*]u8, @ptrCast(ptr))[0..msg.controllen] else message.control, .flags = .{ .eor = (msg.flags & posix.MSG.EOR) != 0, .trunc = (msg.flags & posix.MSG.TRUNC) != 0, @@ -13318,12 +13312,8 @@ fn netReceiveWindows( socket_handle: net.Socket.Handle, message_buffer: []net.IncomingMessage, data_buffer: []u8, - control_buffer: []u8, flags: net.ReceiveFlags, ) struct { ?net.Socket.ReceiveError, usize } { - // Windows doesn't have the concept of control/ancillary data. - _ = control_buffer; - t.netReceiveOneWindows(socket_handle, &message_buffer[0], data_buffer, flags) catch |err| return .{ err, 0 }; return .{ null, 1 }; diff --git a/lib/std/Io/Uring.zig b/lib/std/Io/Uring.zig index 8e0e0c138479d04785de62bd03e23a7f4c908ffc..907d96c6fc7d12264d6a1615ff1f46133dc5914a 100644 --- a/lib/std/Io/Uring.zig +++ b/lib/std/Io/Uring.zig @@ -2095,7 +2095,7 @@ fn operate(userdata: ?*anyopaque, operation: Io.Operation) Io.Cancelable!Io.Oper }, .net_receive => |o| .{ .net_receive = r: { - const opt_err, const n = ev.netReceive(&maybe_sync.cancel_region, o.socket_handle, o.message_buffer, o.data_buffer, o.control_buffer, o.flags); + const opt_err, const n = ev.netReceive(&maybe_sync.cancel_region, o.socket_handle, o.message_buffer, o.data_buffer, o.flags); break :r .{ if (opt_err) |err| switch (err) { error.Canceled => |e| return e, @@ -5075,18 +5075,15 @@ fn netReceive( handle: net.Socket.Handle, message_buffer: []net.IncomingMessage, data_buffer: []u8, - control_buffer: []u8, flags: net.ReceiveFlags, ) struct { ?net.Socket.ReceiveError, usize } { var message_i: usize = 0; var data_i: usize = 0; - var control_i: usize = 0; while (true) { if (message_buffer.len - message_i == 0) return .{ null, message_i }; const message = &message_buffer[message_i]; const remaining_data_buffer = data_buffer[data_i..]; - const remaining_control_buffer = control_buffer[control_i..]; var storage: PosixAddress = undefined; var iov: iovec = .{ .base = remaining_data_buffer.ptr, .len = remaining_data_buffer.len }; var msg: linux.msghdr = .{ @@ -5094,8 +5091,8 @@ fn netReceive( .namelen = @sizeOf(PosixAddress), .iov = (&iov)[0..1], .iovlen = 1, - .control = remaining_control_buffer.ptr, - .controllen = @intCast(remaining_control_buffer.len), + .control = message.control.ptr, + .controllen = @intCast(message.control.len), .flags = undefined, }; @@ -5124,13 +5121,11 @@ fn netReceive( switch (completion.errno()) { .SUCCESS => { const data = remaining_data_buffer[0..@intCast(completion.result)]; - const control = remaining_control_buffer[0..@intCast(msg.controllen)]; data_i += data.len; - control_i += @intCast(msg.controllen); message.* = .{ .from = addressFromPosix(&storage), .data = data, - .control = control, + .control = if (msg.control) |ptr| @as([*]u8, @ptrCast(ptr))[0..msg.controllen] else message.control, .flags = .{ .eor = msg.flags & linux.MSG.EOR != 0, .trunc = msg.flags & linux.MSG.TRUNC != 0, diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig index 9fe2661af17a69649629252ea200407a0151b798..7c5ddbde5e9a711f39d250587c1a4fa95922fd3e 100644 --- a/lib/std/Io/net.zig +++ b/lib/std/Io/net.zig @@ -924,20 +924,22 @@ pub const ReceiveFlags = packed struct(u8) { _: u5 = 0, }; -/// Fields are populated by receive functions. pub const IncomingMessage = struct { + /// Populated by receive functions. from: IpAddress, - /// Points into caller-supplied buffer. + /// Populated by receive functions, points into the caller-supplied buffer. data: []u8, - /// Points into caller-supplied buffer. + /// Supplied by caller before calling receive functions; mutated by receive + /// functions. control: []u8, + /// Populated by receive functions. flags: Flags, - /// Deprecated. Initialize with `undefined`. + /// Useful for initializing before calling `receiveManyTimeout`. pub const init: IncomingMessage = .{ .from = undefined, .data = undefined, - .control = undefined, + .control = &.{}, .flags = undefined, }; @@ -1160,30 +1162,13 @@ pub const Socket = struct { /// Waits for data. Connectionless. /// /// See also: - /// * `receiveWithControl` /// * `receiveTimeout` - /// * `receiveWithControlTimeout` - /// * `receiveManyTimeout` - /// * `receiveManyWithControlTimeout` pub fn receive(s: *const Socket, io: Io, buffer: []u8) ReceiveError!IncomingMessage { - return s.receiveWithControl(io, buffer, &.{}); - } - - /// Waits for data, allowing the reception of control data. Connectionless. - /// - /// See also: - /// * `receive` - /// * `receiveTimeout` - /// * `receiveWithControlTimeout` - /// * `receiveManyTimeout` - /// * `receiveManyWithControlTimeout` - pub fn receiveWithControl(s: *const Socket, io: Io, buffer: []u8, control: []u8) ReceiveError!IncomingMessage { - var message: IncomingMessage = undefined; + var message: IncomingMessage = .init; const maybe_err, const count = (try io.operate(.{ .net_receive = .{ .socket_handle = s.handle, .message_buffer = (&message)[0..1], .data_buffer = buffer, - .control_buffer = control, .flags = .{}, } })).net_receive; if (maybe_err) |err| return err; @@ -1199,42 +1184,18 @@ pub const Socket = struct { /// /// See also: /// * `receive` - /// * `receiveWithControl` - /// * `receiveWithControlTimeout` /// * `receiveManyTimeout` - /// * `receiveManyWithControlTimeout` pub fn receiveTimeout( s: *const Socket, io: Io, buffer: []u8, timeout: Io.Timeout, ) ReceiveTimeoutError!IncomingMessage { - return s.receiveWithControlTimeout(io, buffer, &.{}, timeout); - } - - /// Waits for data, allowing the reception of control data. Connectionless. - /// - /// Returns `error.Timeout` if no message arrives early enough. - /// - /// See also: - /// * `receive` - /// * `receiveWithControl` - /// * `receiveTimeout` - /// * `receiveManyTimeout` - /// * `receiveManyWithControlTimeout` - pub fn receiveWithControlTimeout( - s: *const Socket, - io: Io, - buffer: []u8, - control: []u8, - timeout: Io.Timeout, - ) ReceiveTimeoutError!IncomingMessage { - var message: IncomingMessage = undefined; + var message: IncomingMessage = .init; const maybe_err, const count = (try io.operateTimeout(.{ .net_receive = .{ .socket_handle = s.handle, .message_buffer = (&message)[0..1], .data_buffer = buffer, - .control_buffer = control, .flags = .{}, } }, timeout)).net_receive; if (maybe_err) |err| return err; @@ -1250,47 +1211,21 @@ pub const Socket = struct { /// /// See also: /// * `receive` - /// * `receiveWithControl` /// * `receiveTimeout` - /// * `receiveWithControlTimeout` - /// * `receiveManyWithControlTimeout` pub fn receiveManyTimeout( s: *const Socket, io: Io, + /// Function assumes each element has initialized `control` field. + /// Initializing with `IncomingMessage.init` may be helpful. message_buffer: []IncomingMessage, data_buffer: []u8, flags: ReceiveFlags, timeout: Io.Timeout, - ) struct { ?ReceiveTimeoutError, usize } { - return s.receiveManyWithControlTimeout(io, message_buffer, data_buffer, &.{}, flags, timeout); - } - - /// Waits until at least one message is delivered, possibly returning more - /// than one message. Allows reception of control data. Connectionless. - /// - /// Returns number of messages received, or `error.Timeout` if no message - /// arrives early enough. - /// - /// See also: - /// * `receive` - /// * `receiveWithControl` - /// * `receiveTimeout` - /// * `receiveWithControlTimeout` - /// * `receiveManyTimeout` - pub fn receiveManyWithControlTimeout( - s: *const Socket, - io: Io, - message_buffer: []IncomingMessage, - data_buffer: []u8, - control_buffer: []u8, - flags: ReceiveFlags, - timeout: Io.Timeout, ) struct { ?ReceiveTimeoutError, usize } { const result = io.operateTimeout(.{ .net_receive = .{ .socket_handle = s.handle, .message_buffer = message_buffer, .data_buffer = data_buffer, - .control_buffer = control_buffer, .flags = flags, } }, timeout) catch |err| return .{ err, 0 }; return result.net_receive; -- 2.54.0 From f19259c85b9fc3678d61f8c0d306dc8513b75a39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20R=C3=B8nne=20Petersen?= Date: Thu, 1 Oct 2026 16:04:29 +0200 Subject: [PATCH 10/10] std.c: fix Sigaction layout for sparc64-linux-gnu --- lib/std/c.zig | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/std/c.zig b/lib/std/c.zig index 1ce75c984c1ac06382bfe2ec79303d0ec26ae128..c03e83fd65ac14590516f3a16572f2d3cc6a41df 100644 --- a/lib/std/c.zig +++ b/lib/std/c.zig @@ -3373,6 +3373,19 @@ pub const Sigaction = switch (native_os) { restorer: ?*const fn () callconv(.c) void = null, mask: sigset_t, } else common_linux_Sigaction, + .sparc64 => if (builtin.abi == .gnu) extern struct { + pub const handler_fn = *align(1) const fn (SIG) callconv(.c) void; + pub const sigaction_fn = *const fn (SIG, *const siginfo_t, ?*anyopaque) callconv(.c) void; + + handler: extern union { + handler: ?handler_fn, + sigaction: ?sigaction_fn, + }, + mask: sigset_t, + __glibc_reserved0: c_int = 0, + flags: c_uint, + restorer: ?*const fn () callconv(.c) void = null, + } else common_linux_Sigaction, else => common_linux_Sigaction, }, .emscripten => emscripten.Sigaction, -- 2.54.0