Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(socket) fix error in case of failure/returning a error in the open handler #10154

Merged
merged 11 commits into from
Apr 13, 2024
25 changes: 15 additions & 10 deletions src/bun.js/api/bun/socket.zig
Original file line number Diff line number Diff line change
Expand Up @@ -218,8 +218,9 @@ const Handlers = struct {
this.unprotect();
// will deinit when is not wrapped or when is the TCP wrapped connection
if (wrapped != .tls) {
if (ctx) |ctx_|
if (ctx) |ctx_| {
ctx_.deinit(ssl);
}
}
bun.default_allocator.destroy(this);
}
Expand Down Expand Up @@ -825,23 +826,27 @@ pub const Listener = struct {
const arguments = callframe.arguments(1);
log("close", .{});

if (arguments.len > 0 and arguments.ptr[0].isBoolean() and arguments.ptr[0].toBoolean() and this.socket_context != null) {
this.socket_context.?.close(this.ssl);
this.listener = null;
} else {
var listener = this.listener orelse return JSValue.jsUndefined();
this.listener = null;
listener.close(this.ssl);
}
var listener = this.listener orelse return JSValue.jsUndefined();
this.listener = null;

this.poll_ref.unref(this.handlers.vm);
// if we already have no active connections, we can deinit the context now
if (this.handlers.active_connections == 0) {
this.handlers.unprotect();
this.socket_context.?.close(this.ssl);
// deiniting the context will also close the listener
this.socket_context.?.deinit(this.ssl);
this.socket_context = null;
this.strong_self.clear();
this.strong_data.clear();
} else {
const forceClose = arguments.len > 0 and arguments.ptr[0].isBoolean() and arguments.ptr[0].toBoolean() and this.socket_context != null;
if (forceClose) {
// close all connections in this context and wait for them to close
this.socket_context.?.close(this.ssl);
} else {
// only close the listener and wait for the connections to close by it self
listener.close(this.ssl);
}
}

return JSValue.jsUndefined();
Expand Down
43 changes: 43 additions & 0 deletions src/deps/uws.zig
Original file line number Diff line number Diff line change
Expand Up @@ -893,6 +893,47 @@ pub const SocketContext = opaque {
us_socket_context_free(@as(i32, 0), this);
}

pub fn cleanCallbacks(ctx: *SocketContext, is_ssl: bool) void {
const ssl_int: i32 = @intFromBool(is_ssl);
// replace callbacks with dummy ones
const DummyCallbacks = struct {
fn open(socket: *Socket, _: i32, _: [*c]u8, _: i32) callconv(.C) ?*Socket {
return socket;
}
fn close(socket: *Socket, _: i32, _: ?*anyopaque) callconv(.C) ?*Socket {
return socket;
}
fn data(socket: *Socket, _: [*c]u8, _: i32) callconv(.C) ?*Socket {
return socket;
}
fn writable(socket: *Socket) callconv(.C) ?*Socket {
return socket;
}
fn timeout(socket: *Socket) callconv(.C) ?*Socket {
return socket;
}
fn connect_error(socket: *Socket, _: i32) callconv(.C) ?*Socket {
return socket;
}
fn end(socket: *Socket) callconv(.C) ?*Socket {
return socket;
}
fn handshake(_: *Socket, _: i32, _: us_bun_verify_error_t, _: ?*anyopaque) callconv(.C) void {}
fn long_timeout(socket: *Socket) callconv(.C) ?*Socket {
return socket;
}
};
us_socket_context_on_open(ssl_int, ctx, DummyCallbacks.open);
us_socket_context_on_close(ssl_int, ctx, DummyCallbacks.close);
us_socket_context_on_data(ssl_int, ctx, DummyCallbacks.data);
us_socket_context_on_writable(ssl_int, ctx, DummyCallbacks.writable);
us_socket_context_on_timeout(ssl_int, ctx, DummyCallbacks.timeout);
us_socket_context_on_connect_error(ssl_int, ctx, DummyCallbacks.connect_error);
us_socket_context_on_end(ssl_int, ctx, DummyCallbacks.end);
us_socket_context_on_handshake(ssl_int, ctx, DummyCallbacks.handshake, null);
us_socket_context_on_long_timeout(ssl_int, ctx, DummyCallbacks.long_timeout);
}

fn getLoop(this: *SocketContext, ssl: bool) ?*Loop {
if (ssl) {
return us_socket_context_loop(@as(i32, 1), this);
Expand All @@ -902,6 +943,8 @@ pub const SocketContext = opaque {

/// closes and deinit the SocketContexts
pub fn deinit(this: *SocketContext, ssl: bool) void {
// we clean the callbacks to avoid UAF because we are deiniting
this.cleanCallbacks(ssl);
this.close(ssl);
//always deinit in next iteration
if (ssl) {
Expand Down
87 changes: 86 additions & 1 deletion test/js/bun/net/socket.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { expect, it } from "bun:test";
import { bunEnv, bunExe, expectMaxObjectTypeCount, isWindows } from "harness";
import { connect, fileURLToPath, SocketHandler, spawn } from "bun";

import type { Socket } from "bun";
it("should coerce '0' to 0", async () => {
const listener = Bun.listen({
// @ts-expect-error
Expand Down Expand Up @@ -271,3 +271,88 @@ it("socket.timeout works", async () => {
it("should allow large amounts of data to be sent and received", async () => {
expect([fileURLToPath(new URL("./socket-huge-fixture.js", import.meta.url))]).toRun();
}, 60_000);

it("it should not crash when getting a ReferenceError on client socket open", async () => {
const server = Bun.serve({
port: 8080,
hostname: "localhost",
fetch() {
return new Response("Hello World");
},
});
try {
const { resolve, reject, promise } = Promise.withResolvers();
let client: Socket<undefined> | null = null;
const timeout = setTimeout(() => {
client?.end();
reject(new Error("Timeout"));
}, 1000);
client = await Bun.connect({
port: server.port,
hostname: server.hostname,
socket: {
open(socket) {
// ReferenceError: Can't find variable: bytes
// @ts-expect-error
socket.write(bytes);
},
error(socket, error) {
clearTimeout(timeout);
resolve(error);
},
close(socket) {
// we need the close handler
resolve({ message: "Closed" });
},
data(socket, data) {},
},
});

const result: any = await promise;
expect(result?.message).toBe("Can't find variable: bytes");
} finally {
server.stop(true);
}
});

it("it should not crash when returning a Error on client socket open", async () => {
const server = Bun.serve({
port: 8080,
hostname: "localhost",
fetch() {
return new Response("Hello World");
},
});
try {
const { resolve, reject, promise } = Promise.withResolvers();
let client: Socket<undefined> | null = null;
const timeout = setTimeout(() => {
client?.end();
reject(new Error("Timeout"));
}, 1000);
client = await Bun.connect({
port: server.port,
hostname: server.hostname,
socket: {
//@ts-ignore
open(socket) {
return new Error("CustomError");
},
error(socket, error) {
clearTimeout(timeout);
resolve(error);
},
close(socket) {
// we need the close handler
resolve({ message: "Closed" });
},
data(socket, data) {},
},
});

const result: any = await promise;
expect(result?.message).toBe("CustomError");
} finally {
server.stop(true);
}
});
Loading