Files
bun.sh/test/js/bun/http/bun-websocket-cpu-fixture.js
Ciro Spaciari aef0b5b4a6 fix(usockets): safely handle socket reallocation during context adoption (#25361)
## Summary
- Fix use-after-free vulnerability during socket adoption by properly
tracking reallocated sockets
- Add safety checks to prevent linking closed sockets to context lists
- Properly track socket state with new `is_closed`, `adopted`, and
`is_tls` flags

## What does this PR do?

This PR improves event loop stability by addressing potential
use-after-free issues that can occur when sockets are reallocated during
adoption (e.g., when upgrading a TCP socket to TLS).

### Key Changes

**Socket State Tracking
([internal.h](packages/bun-usockets/src/internal/internal.h))**
- Added `is_closed` flag to explicitly track when a socket has been
closed
- Added `adopted` flag to mark sockets that were reallocated during
context adoption
- Added `is_tls` flag to track TLS socket state for proper low-priority
queue handling

**Safe Socket Adoption
([context.c](packages/bun-usockets/src/context.c))**
- When `us_poll_resize()` returns a new pointer (reallocation occurred),
the old socket is now:
  - Marked as closed (`is_closed = 1`)
  - Added to the closed socket cleanup list
  - Marked as adopted (`adopted = 1`)
  - Has its `prev` pointer set to the new socket for event redirection
- Added guards to
`us_internal_socket_context_link_socket/listen_socket/connecting_socket`
to prevent linking already-closed sockets

**Event Loop Handling ([loop.c](packages/bun-usockets/src/loop.c))**
- After callbacks that can trigger socket adoption (`on_open`,
`on_writable`, `on_data`), the event loop now checks if the socket was
reallocated and redirects to the new socket
- Low-priority socket handling now properly checks `is_closed` state and
uses `is_tls` flag for correct SSL handling

**Poll Resize Safety
([epoll_kqueue.c](packages/bun-usockets/src/eventing/epoll_kqueue.c))**
- Changed `us_poll_resize()` to always allocate new memory with
`us_calloc()` instead of `us_realloc()` to ensure the old pointer
remains valid for cleanup
- Now takes `old_ext_size` parameter to correctly calculate memory sizes
- Re-enabled `us_internal_loop_update_pending_ready_polls()` call in
`us_poll_change()` to ensure pending events are properly redirected

### How did you verify your code works?
Run existing CI and existing socket upgrade tests under asan build
2025-12-15 18:43:51 -08:00

68 lines
1.9 KiB
JavaScript
Generated

import path from "path";
const server = Bun.serve({
port: 0,
idleTimeout: 100,
tls: {
cert: Bun.file(path.join(import.meta.dir, "fixtures", "cert.pem")),
key: Bun.file(path.join(import.meta.dir, "fixtures", "cert.key")),
},
fetch(req, server) {
if (server.upgrade(req)) {
return;
}
return new Response("Upgrade failed", { status: 500 });
},
websocket: {
idleTimeout: 120,
open(ws) {},
message(ws, message) {
ws.send(message);
},
},
});
const ws = new WebSocket(`wss://${server.hostname}:${server.port}`, { tls: { rejectUnauthorized: false } });
const { promise: openWS, resolve: onWSOpen } = Promise.withResolvers();
ws.onopen = onWSOpen;
await openWS;
for (let i = 0; i < 1000; i++) {
ws.send("hello");
}
let bytesReceived = 0;
ws.onmessage = event => {
bytesReceived += event.data.length;
};
let previousUsage = process.cpuUsage();
let previousTime = Date.now();
let count = 0;
setInterval(() => {
count++;
const currentUsage = process.cpuUsage(previousUsage);
const currentTime = Date.now();
const userCpuTime = currentUsage.user; // microseconds
const systemCpuTime = currentUsage.system; // microseconds
const totalCpuTime = userCpuTime + systemCpuTime;
const timeDeltaMs = currentTime - previousTime; // milliseconds
const timeDeltaMicroseconds = timeDeltaMs * 1000; // convert to microseconds
// Calculate percentage for the current process
const cpuUsagePercentage = (totalCpuTime / timeDeltaMicroseconds) * 100;
console.log(`CPU Usage: ${cpuUsagePercentage.toFixed(2)}%`);
previousUsage = process.cpuUsage(); // Update for the next interval
previousTime = currentTime;
if (count == 3) {
server.stop(true);
// The expected value is around 0.XX%, but we allow a 2% margin of error to account for potential flakiness.
process.exit(cpuUsagePercentage < 2 ? 0 : 1);
}
}, 1000);