authorgravatar for hemisputnik@proton.mehemisputnik <hemisputnik@proton.me> 2026-07-23 03:08:48+03:00
committergravatar for mail@isaacfreund.comIsaac Freund <mail@isaacfreund.com> 2026-10-01 10:55:12+02:00
logb3e8eb657772e46058ab011eb875b43d181e56c0
treeab14c944f8f0a0bf86d852766a4d1fe4857202fa
parentfaa0a4a54ec9ff86100e306457b631e3cb23773d
signature Commit is signed but in an unrecognized format.

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.

1 files changed, 114 insertions(+), 0 deletions(-)

lib/std/Io/net/test.zig+114
...@@ -295,6 +295,120 @@ test "listen on a unix socket, send bytes, receive bytes" {...@@ -295,6 +295,120 @@ test "listen on a unix socket, send bytes, receive bytes" {
295 try client_task.await(io);295 try client_task.await(io);
296}296}
297297
298test "listen on a unix socket, pass file descriptor" {
299 if (builtin.os.tag == .windows or builtin.os.tag == .wasi) {
300 // Windows and WASI don't have the concept of control/ancillary data.
301 return error.SkipZigTest;
302 }
303
304 const io = testing.io;
305 const gpa = testing.allocator;
306
307 const socket_path = try generateFileName(gpa, io, "socket.unix");
308 defer gpa.free(socket_path);
309 const temp_file_path = try generateFileName(gpa, io, "temp_file");
310 defer gpa.free(temp_file_path);
311
312 defer Io.Dir.cwd().deleteFile(io, socket_path) catch {};
313 defer Io.Dir.cwd().deleteFile(io, temp_file_path) catch {};
314
315 const socket_addr = try net.UnixAddress.init(socket_path);
316
317 var server = socket_addr.listen(io, .{}) catch |err| switch (err) {
318 error.AddressFamilyUnsupported => return error.SkipZigTest,
319 error.NetworkDown => return error.SkipZigTest,
320 else => |e| return e,
321 };
322 defer server.socket.close(io);
323
324 const cmsg_alignment = switch (builtin.os.tag) {
325 .driverkit,
326 .ios,
327 .maccatalyst,
328 .macos,
329 .tvos,
330 .visionos,
331 .watchos,
332 => 4,
333 else => @sizeOf(usize),
334 };
335
336 const cmsghdr_aligned_len = comptime mem.alignForward(usize, @sizeOf(std.posix.cmsghdr), cmsg_alignment);
337 const cmsghdr_align = @alignOf(std.posix.cmsghdr);
338 const fd_t_len = @sizeOf(std.posix.fd_t);
339
340 const cmsg_buf_len =
341 cmsghdr_aligned_len +
342 comptime mem.alignForward(usize, fd_t_len, cmsg_alignment);
343
344 const S = struct {
345 fn clientFn(path: []const u8, file_path: []const u8) !void {
346 const temp_file = try Io.Dir.cwd().createFile(io, file_path, .{});
347 defer temp_file.close(io);
348
349 const server_path: net.UnixAddress = try .init(path);
350 var stream = try server_path.connect(io);
351 defer stream.close(io);
352
353 var cmsg_buf: [cmsg_buf_len]u8 align(cmsghdr_align) = @splat(0);
354
355 const header_ptr: *std.posix.cmsghdr = @ptrCast(&cmsg_buf);
356 header_ptr.* = .{
357 .len = @intCast(cmsghdr_aligned_len + fd_t_len),
358 .level = std.posix.SOL.SOCKET,
359 .type = std.posix.SCM.RIGHTS,
360 };
361
362 const fds = mem.bytesAsSlice(std.posix.fd_t, cmsg_buf[cmsghdr_aligned_len..]);
363 fds[0] = temp_file.handle;
364
365 var stream_writer = stream.writer(io, &.{});
366 stream_writer.control = &cmsg_buf;
367 try stream_writer.interface.writeAll("Hello world!");
368 }
369 };
370
371 var client_task = io.concurrent(S.clientFn, .{ socket_path, temp_file_path }) catch |err| switch (err) {
372 error.ConcurrencyUnavailable => return error.SkipZigTest,
373 };
374 defer client_task.cancel(io) catch {};
375
376 var stream = try server.accept(io);
377 defer stream.close(io);
378
379 var control_buf: [32]u8 align(cmsghdr_align) = undefined;
380 var buf: [16]u8 = undefined;
381 var stream_reader = stream.readerWithControl(io, &.{}, &control_buf);
382
383 const n = try stream_reader.interface.readSliceShort(&buf);
384 const cmsg_buf = stream_reader.controlSlice();
385
386 try testing.expect(!stream_reader.control_truncated); // The control buffer should be big enough to receive the entire control message
387
388 try testing.expectEqual(12, n);
389 try testing.expectEqualStrings("Hello world!", buf[0..n]);
390
391 try testing.expect(cmsg_buf.len >= cmsghdr_aligned_len + fd_t_len);
392
393 const header_ptr: *const std.posix.cmsghdr = @ptrCast(@alignCast(cmsg_buf));
394
395 try testing.expectEqual(cmsghdr_aligned_len + fd_t_len, header_ptr.len);
396 try testing.expectEqual(std.posix.SOL.SOCKET, header_ptr.level);
397 try testing.expectEqual(std.posix.SCM.RIGHTS, header_ptr.type);
398
399 const fds = mem.bytesAsSlice(std.posix.fd_t, cmsg_buf[cmsghdr_aligned_len..]);
400
401 const temp_file: Io.File = .{
402 .handle = fds[0],
403 .flags = .{ .nonblocking = false },
404 };
405 defer temp_file.close(io);
406 var temp_file_writer = temp_file.writer(io, &.{});
407 try temp_file_writer.interface.writeAll("if this is a real file descriptor, this shouldn't fail");
408
409 try client_task.await(io);
410}
411
298fn generateFileName(gpa: Allocator, io: Io, base_name: []const u8) ![]const u8 {412fn generateFileName(gpa: Allocator, io: Io, base_name: []const u8) ![]const u8 {
299 const random_bytes_count = 12;413 const random_bytes_count = 12;
300 const sub_path_len = comptime std.base64.url_safe.Encoder.calcSize(random_bytes_count);414 const sub_path_len = comptime std.base64.url_safe.Encoder.calcSize(random_bytes_count);