From 65bec859fa17bb6d7b21c5e6574e7895561228c8 Mon Sep 17 00:00:00 2001 From: Endel Dreyer Date: Fri, 30 Oct 2026 17:10:39 -0300 Subject: [PATCH] iocp: report a shut-down socket instead of panicking recv and send on a socket that had been shut down fell through to unexpectedWSAError, which cast the code to Win32Error. WSA codes aren't in that enum, so safe builds panicked. POSIX reports the same race as EOF and EPIPE, and any other unmapped WSA code took the same path. Assisted-by: Claude Opus 5.5 --- src/backend/iocp.zig | 62 ++++++++++++++++++++++++++++++++++++++++++-- src/windows.zig | 4 ++- 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/src/backend/iocp.zig b/src/backend/iocp.zig index cc8506ea..34df3e56 100644 --- a/src/backend/iocp.zig +++ b/src/backend/iocp.zig @@ -634,7 +634,7 @@ pub const Loop = struct { break :action switch (err) { windows.ws2_32.WinsockError.WSA_IO_PENDING => .{ .submitted = {} }, .WSA_OPERATION_ABORTED, .WSAECONNABORTED => .{ .result = .{ .send = error.Canceled } }, - .WSAECONNRESET, .WSAENETRESET => .{ .result = .{ .send = error.ConnectionResetByPeer } }, + .WSAECONNRESET, .WSAENETRESET, .WSAESHUTDOWN => .{ .result = .{ .send = error.ConnectionResetByPeer } }, else => .{ .result = .{ .send = windows.unexpectedWSAError(err) } }, }; } @@ -663,6 +663,7 @@ pub const Loop = struct { windows.ws2_32.WinsockError.WSA_IO_PENDING => .{ .submitted = {} }, .WSA_OPERATION_ABORTED, .WSAECONNABORTED => .{ .result = .{ .recv = error.Canceled } }, .WSAECONNRESET, .WSAENETRESET => .{ .result = .{ .recv = error.ConnectionResetByPeer } }, + .WSAESHUTDOWN => .{ .result = .{ .recv = error.EOF } }, else => .{ .result = .{ .recv = windows.unexpectedWSAError(err) } }, }; } @@ -1095,7 +1096,7 @@ pub const Completion = struct { return .{ .send = switch (err) { .WSA_OPERATION_ABORTED, .WSAECONNABORTED => error.Canceled, - .WSAECONNRESET, .WSAENETRESET => error.ConnectionResetByPeer, + .WSAECONNRESET, .WSAENETRESET, .WSAESHUTDOWN => error.ConnectionResetByPeer, else => windows.unexpectedWSAError(err), }, }; @@ -1115,6 +1116,7 @@ pub const Completion = struct { .recv = switch (err) { .WSA_OPERATION_ABORTED, .WSAECONNABORTED => error.Canceled, .WSAECONNRESET, .WSAENETRESET => error.ConnectionResetByPeer, + .WSAESHUTDOWN => error.EOF, else => windows.unexpectedWSAError(err), }, }; @@ -2012,6 +2014,62 @@ test "iocp: file IO with offset" { try testing.expectEqualSlices(u8, &write_buf, read_buf[0..read_len]); } +test "iocp: recv and send on a socket that was shut down" { + const mem = std.mem; + const testing = std.testing; + + var loop = try Loop.init(.{}); + defer loop.deinit(); + + const address = try net.Address.parseIp4("127.0.0.1", 3133); + const ln = try windows.WSASocketW(windows.ws2_32.AF.INET, windows.ws2_32.SOCK.STREAM, windows.ws2_32.IPPROTO.TCP, null, 0, windows.ws2_32.WSA_FLAG_OVERLAPPED); + defer iocpClose(ln); + try iocpSetsockopt(asSocket(ln), windows.ws2_32.SOL.SOCKET, windows.ws2_32.SO.REUSEADDR, &mem.toBytes(@as(c_int, 1))); + try iocpBind(asSocket(ln), &address.any, address.getOsSockLen()); + try iocpListen(asSocket(ln), 1); + + const conn = try windows.WSASocketW(windows.ws2_32.AF.INET, windows.ws2_32.SOCK.STREAM, windows.ws2_32.IPPROTO.TCP, null, 0, windows.ws2_32.WSA_FLAG_OVERLAPPED); + defer iocpClose(conn); + try testing.expectEqual(0, windows.ws2_32.connect(asSocket(conn), &address.any, @intCast(address.getOsSockLen()))); + try iocpShutdown(asSocket(conn), .both); + + const Outcome = struct { + recv: ?ReadError!usize = null, + send: ?WriteError!usize = null, + + fn onRecv(ud: ?*anyopaque, _: *Loop, _: *Completion, r: Result) CallbackAction { + const self: *@This() = @ptrCast(@alignCast(ud.?)); + self.recv = r.recv; + return .disarm; + } + + fn onSend(ud: ?*anyopaque, _: *Loop, _: *Completion, r: Result) CallbackAction { + const self: *@This() = @ptrCast(@alignCast(ud.?)); + self.send = r.send; + return .disarm; + } + }; + + var outcome: Outcome = .{}; + var buf: [8]u8 = undefined; + var c_recv: Completion = .{ + .op = .{ .recv = .{ .fd = conn, .buffer = .{ .slice = &buf } } }, + .userdata = &outcome, + .callback = Outcome.onRecv, + }; + var c_send: Completion = .{ + .op = .{ .send = .{ .fd = conn, .buffer = .{ .slice = "ping" } } }, + .userdata = &outcome, + .callback = Outcome.onSend, + }; + loop.add(&c_recv); + loop.add(&c_send); + try loop.run(.until_done); + + try testing.expectError(error.EOF, outcome.recv.?); + try testing.expectError(error.ConnectionResetByPeer, outcome.send.?); +} + test "iocp: socket accept/connect/send/recv/close" { const mem = std.mem; const testing = std.testing; diff --git a/src/windows.zig b/src/windows.zig index 8ddbe3a3..54f771f6 100644 --- a/src/windows.zig +++ b/src/windows.zig @@ -459,7 +459,9 @@ pub const ws2_32 = struct { // --- High-level wrapper functions --- pub fn unexpectedWSAError(err: ws2_32.WinsockError) error{Unexpected} { - return unexpectedError(@as(Win32Error, @enumFromInt(@intFromEnum(err)))); + // Not `unexpectedError`: WSA codes aren't in `Win32Error`, and casting one panics. + std.log.debug("unexpected Winsock error: {d}", .{@intFromEnum(err)}); + return error.Unexpected; } pub fn QueryPerformanceCounter() u64 {