Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions packages/bun-usockets/src/socket.c
Original file line number Diff line number Diff line change
Expand Up @@ -724,12 +724,11 @@
}

void us_internal_socket_raw_shutdown(struct us_socket_t *s) {
/* Todo: should we emit on_close if calling shutdown on an already half-closed socket?
* We need more states in that case, we need to track RECEIVED_FIN
* so far, the app has to track this and call close as needed */
if (!us_socket_is_closed(s) && us_internal_poll_type(&s->p) != POLL_TYPE_SOCKET_SHUT_DOWN) {
us_internal_poll_set_type(&s->p, POLL_TYPE_SOCKET_SHUT_DOWN);
us_poll_change(&s->p, s->group->loop, us_poll_events(&s->p) & LIBUS_SOCKET_READABLE);
/* After read_eof the poll may be at 0 events; re-arm READABLE so the SHUT_DOWN eof branch closes us. */
us_poll_change(&s->p, s->group->loop,
s->read_eof ? LIBUS_SOCKET_READABLE : (us_poll_events(&s->p) & LIBUS_SOCKET_READABLE));

Check warning on line 731 in packages/bun-usockets/src/socket.c

View check run for this annotation

Claude / Claude Code Review

kqueue: read_eof arm in raw_shutdown makes the sentinel unreachable; a later resume() can then delete the read filter and strand the socket

On kqueue, the `read_eof` arm now leaves `us_poll_events == READABLE`, so the sentinel branch at line 734 (`!(events & READABLE)`) is unreachable in exactly the case this PR targets — the PR body's "kqueue is covered by the read sentinel" only holds while nothing later calls `us_poll_change(0)`. `us_socket_resume` on a paused, `read_eof`, shut-down socket does exactly that (its `is_shut_down` branch passes `readable = 0`), which pre-PR was an old==new no-op but post-PR goes R→0: kqueue EV_DELETE
Comment thread
robobun marked this conversation as resolved.
Comment thread
claude[bot] marked this conversation as resolved.
bsd_shutdown_socket(us_poll_fd((struct us_poll_t *) s));
#ifdef LIBUS_USE_KQUEUE
if (!(us_poll_events(&s->p) & LIBUS_SOCKET_READABLE)) {
Expand Down
45 changes: 45 additions & 0 deletions test/js/bun/net/socket.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3827,3 +3827,48 @@ describe("allowHalfOpen socket whose peer resets behind pending writes", () => {
expect(endCount).toBe(1);
});
});

describe("allowHalfOpen socket shut down after the peer's FIN", () => {
// Once end() has been delivered the poll drops readable interest and, with
// nothing queued, settles with no events armed. shutdown() must re-arm it so
// the fully-closed state is delivered; unfixed, the libuv backend stranded.
it("closes when shutdown() runs after the poll has settled", async () => {
const ended = Promise.withResolvers<Socket>();
const closed = Promise.withResolvers<void>();

using server = Bun.listen({
hostname: "127.0.0.1",
port: 0,
allowHalfOpen: true,
socket: {
open() {},
data() {},
end: s => ended.resolve(s),
error() {},
Comment thread
robobun marked this conversation as resolved.
Outdated
close: () => closed.resolve(),
},
});

const peerClosed = Promise.withResolvers<void>();
const peer = await Bun.connect({
hostname: "127.0.0.1",
port: server.port,
allowHalfOpen: true,
socket: {
open: s => s.shutdown(), // FIN
data() {},
end() {},
error() {},
close: () => peerClosed.resolve(),
},
});

const victim = await ended.promise;
// Let the writable side drain and the poll settle before shutting down.
for (let i = 0; i < 5; i++) await new Promise<void>(r => setImmediate(r));
Comment thread
robobun marked this conversation as resolved.
Outdated
victim.shutdown();
await closed.promise;
await peerClosed.promise;
peer.end();
Comment thread
robobun marked this conversation as resolved.
Outdated
});
});