Skip to content
Closed

ai slop #33018

Show file tree
Hide file tree
Changes from all 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
32 changes: 29 additions & 3 deletions packages/bun-usockets/src/eventing/libuv.c
Original file line number Diff line number Diff line change
Expand Up @@ -181,11 +181,14 @@ static void close_cb_free(uv_handle_t *h) { us_free(h->data); }

/* This one is different for polls, since we need two frees here */
static void close_cb_free_poll(uv_handle_t *h) {
/* It is only in case we called us_poll_stop then quickly us_poll_free that we
* enter this. Most of the time, actual freeing is done by us_poll_free. */
/* us_poll_free normally re-pointed data at the us_poll_t before this runs.
* If a nested tick's loop_post deferred the closed-socket sweep (see
* tick_depth in us_loop_run), we run first: mark the handle for us_poll_free. */
if (h->data) {
us_free(h->data);
us_free(h);
} else {
h->data = h;
}
}
Comment thread
robobun marked this conversation as resolved.

Expand Down Expand Up @@ -213,6 +216,14 @@ void us_poll_free(struct us_poll_t *p, struct us_loop_t *loop) {
us_free(p);
return;
}
/* close_cb_free_poll already ran: a nested tick deferred this sweep to the
* outermost loop_post (tick_depth), so libuv finished closing the handle
* before we got here. It is done with uv_p; we free both. */
if (p->uv_p->data == (void *)p->uv_p) {
us_free(p->uv_p);
us_free(p);
return;
}
/* The idea here is like so; in us_poll_stop we call uv_close after setting
* data of uv-poll to 0. This means that in close_cb_free we call free on 0
* with does nothing, since us_poll_stop should not really free the poll.
Expand Down Expand Up @@ -333,10 +344,19 @@ void us_loop_pump(struct us_loop_t *loop) {
* for unref'd handles (subprocess exit packets, socket events) and due
* timers are never processed. Bun's outer drive loops (wait_for_promise,
* bun:test) supply their own keep-going predicate, so force exactly one
* non-blocking iteration; UV_RUN_NOWAIT keeps the poll timeout at 0. */
* non-blocking iteration; UV_RUN_NOWAIT keeps the poll timeout at 0.
*
* Bun enters here instead of us_loop_run whenever nothing ref's the loop, so
* this can be the outermost tick of a close-then-re-enter sequence: a poll
* callback dispatched by this iteration closes its socket (uv_close makes
* the loop active again) and re-enters through us_loop_run. That nested
* tick's loop_post must see depth 2, or it sweeps the socket this
* iteration's dispatch still holds. See us_internal_loop_post. */
loop->data.tick_depth++;
Comment thread
robobun marked this conversation as resolved.
loop->uv_loop->active_handles++;
uv_run(loop->uv_loop, UV_RUN_NOWAIT);
loop->uv_loop->active_handles--;
loop->data.tick_depth--;
}

struct us_loop_t *us_create_loop(void *hint,
Expand Down Expand Up @@ -405,6 +425,11 @@ void us_loop_run(struct us_loop_t *loop) {
us_loop_integrate(loop);
uv_update_time(loop->uv_loop);

/* us_internal_loop_post runs from the uv_check_t registered above, so it
* fires inside uv_run; the tick_depth bracket mirrors us_loop_run and
* us_loop_run_bun_tick in epoll_kqueue.c. See us_internal_loop_post. */
loop->data.tick_depth++;

/* UV_RUN_ONCE may block in the poll phase (pending callbacks dispatch
* first), making this the JS thread's park hook, the counterpart of
* us_loop_run_bun_tick's. jsc_vm is only set on the JS thread's loop. */
Expand All @@ -415,6 +440,7 @@ void us_loop_run(struct us_loop_t *loop) {
}

uv_run(loop->uv_loop, UV_RUN_ONCE);
loop->data.tick_depth--;
}

struct us_poll_t *us_create_poll(struct us_loop_t *loop, int fallthrough,
Expand Down
9 changes: 5 additions & 4 deletions packages/bun-usockets/src/internal/loop_data.h
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,11 @@ struct us_internal_loop_data_t {
/* We do not care if this flips or not, it doesn't matter */
size_t iteration_nr;
void* jsc_vm;
/* Reentrancy depth of us_loop_run_bun_tick. When >1, we are inside a
* nested tick (e.g. waitForPromise from a poll callback). Freeing closed
* sockets must be deferred to the outermost tick so the outer dispatch
* doesn't read a freed poll. */
/* Reentrancy depth of the loop-run entry points (us_loop_run_bun_tick and
* us_loop_run on epoll/kqueue; us_loop_run and us_loop_pump on libuv).
* When >1 we are inside a nested tick (e.g. waitForPromise from a poll
* callback). Freeing closed sockets must be deferred to the outermost
* tick so the outer dispatch doesn't read a freed poll. */
int tick_depth;
};

Expand Down
29 changes: 29 additions & 0 deletions patches/libuv/win-poll-no-reendgame-after-close.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
--- a/src/win/poll.c
+++ b/src/win/poll.c
@@ -217,8 +217,14 @@ static void uv__fast_poll_process_poll_req(uv_loop_t* loop, uv_poll_t* handle,
handle->submitted_events_2)) != 0) {
uv__fast_poll_submit_poll_req(loop, handle);
} else if ((handle->flags & UV_HANDLE_CLOSING) &&
+ !(handle->flags & UV_HANDLE_CLOSED) &&
handle->submitted_events_1 == 0 &&
handle->submitted_events_2 == 0) {
+ /* A nested uv_run entered from inside poll_cb may have already run this
+ * handle's endgame: uv__process_endgames cleared ENDGAME_QUEUED and
+ * uv__handle_close set CLOSED. Re-queuing it here would run the endgame
+ * again, which uv__poll_endgame asserts against and which invokes
+ * close_cb a second time. */
uv__want_endgame(loop, (uv_handle_t*) handle);
}
}
@@ -418,8 +424,11 @@ static void uv__slow_poll_process_poll_req(uv_loop_t* loop, uv_poll_t* handle,
handle->submitted_events_2)) != 0) {
uv__slow_poll_submit_poll_req(loop, handle);
} else if ((handle->flags & UV_HANDLE_CLOSING) &&
+ !(handle->flags & UV_HANDLE_CLOSED) &&
handle->submitted_events_1 == 0 &&
handle->submitted_events_2 == 0) {
+ /* Same as the fast path: never re-queue an endgame that a nested uv_run
+ * already ran for this handle. */
uv__want_endgame(loop, (uv_handle_t*) handle);
}
}
13 changes: 12 additions & 1 deletion scripts/build/deps/libuv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,18 @@ export const libuv: Dependency = {
// an in-process loopback fetch().abort() can fall into. To upstream:
// send to libuv/libuv with the wepoll/ReactOS references in the patch
// comment as the rationale.
patches: ["patches/libuv/win-poll-rearm-before-callback.patch", "patches/libuv/win-poll-abort-with-disconnect.patch"],
//
// win-poll-no-reendgame-after-close: the post-poll_cb endgame check in
// uv__fast_poll_process_poll_req (and its slow-poll sibling) re-queues a
// handle whose endgame a nested uv_run, entered from inside poll_cb,
// already ran, which double-invokes close_cb. Guard the check on
// !(flags & UV_HANDLE_CLOSED), which uv__poll_endgame already asserts.
// Nested uv_run is outside libuv's contract, so this is not upstreamable.
patches: [
"patches/libuv/win-poll-rearm-before-callback.patch",
"patches/libuv/win-poll-abort-with-disconnect.patch",
"patches/libuv/win-poll-no-reendgame-after-close.patch",
],

build: () => ({
kind: "direct",
Expand Down
104 changes: 104 additions & 0 deletions test/js/bun/net/socket.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4157,3 +4157,107 @@ describe.concurrent.each(["tcp", "tls"] as const)("%s socket paused when its pee
});
});
});

// On the libuv (Windows) event loop backend, a synchronous re-entrant loop
// tick from inside a socket's data callback ran the closed-socket sweep and
// freed the us_socket_t the suspended outer dispatch frame still held.
//
// Bun picks the uws entry point at the top of each tick from libuv's
// active_handles count: non-zero => us_loop_run, zero => us_loop_pump. The
// two cases drive the same close-then-re-enter sequence through each one.
// On Windows the subprocess also reports active_handles as seen from inside
// data(): 3 in the ref'd case (listener, accepted socket, client), and 1 in
// the unref'd case, which is us_loop_pump's own increment, i.e. proof that
// the dispatch really came through the pump.
describe.concurrent.each([
{ name: "ref'd (us_loop_run)", unref: false, activeHandlesInData: 3 },
{ name: "unref'd (us_loop_pump)", unref: true, activeHandlesInData: 1 },
])(
"closing a socket and re-entering the event loop from its own data callback, $name",
({ unref, activeHandlesInData }) => {
it("survives", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const UNREF = ${unref};
const { getEventLoopStats } = require("bun:internal-for-testing");
// The setImmediate callback only runs inside the nested tick that
// transform() spins while it waits for the handler's promise, so
// reentries === 20 proves every round really re-entered the loop.
let reentries = 0;
const activeHandles = new Set();
const server = Bun.listen({
hostname: "127.0.0.1",
port: 0,
socket: {
open(sock) {
if (UNREF) sock.unref();
},
data(sock) {
activeHandles.add(getEventLoopStats().numPolls);
// Synchronously close the socket, then synchronously re-enter
// the event loop. transform() ticks the loop until the handler's
// promise settles and then throws because it cannot return the
// result synchronously; the nested tick is the part that matters.
sock.terminate();
try {
new HTMLRewriter()
.on("p", { element: () => new Promise(r => setImmediate(() => { reentries++; r(); })) })
.transform("<p></p>");
} catch {}
},
close() {},
error() {},
},
});
if (UNREF) server.unref();
for (let i = 0; i < 20; i++) {
const { promise, resolve } = Promise.withResolvers();
Bun.connect({
hostname: "127.0.0.1",
port: server.port,
socket: {
open(s) {
if (UNREF) s.unref();
// Write from the top of the next tick rather than from inside
// open(): the immediate releases its own keep-alive before the
// tick chooses its entry point, so in the unref'd case the tick
// that dispatches data() sees zero active handles.
setImmediate(() => s.write("x"));
},
data() {},
close: resolve,
end: resolve,
error: resolve,
connectError: resolve,
},
}).catch(resolve);
await promise;
}
server.stop(true);
console.log(JSON.stringify({ reentries, activeHandles: [...activeHandles] }));
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});

const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// A crashed child prints nothing; keep the parse from throwing so the
// failure diff still shows its stderr and exit code.
expect({ result: stdout && JSON.parse(stdout), stderr, exitCode }).toEqual({
result: {
reentries: 20,
// numPolls is libuv's active_handles on Windows; on the other
// backends it counts polls and the run/pump distinction does not exist.
activeHandles: isWindows ? [activeHandlesInData] : expect.any(Array),
},
stderr: expect.any(String),
exitCode: 0,
});
});
},
);
Loading