Skip to content
Merged
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
2 changes: 1 addition & 1 deletion packages/bun-types/bun.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6306,7 +6306,7 @@ declare module "bun" {
/**
* Finalize the hash
*
* @param hashInto `TypedArray` to write the hash into. Faster than creating a new one each time. It must hold at least {@link byteLength} bytes.
* @param hashInto `TypedArray` to write the hash into. Faster than creating a new one each time. It must hold at least {@link byteLength} bytes. `shake128` and `shake256` fill all of it.
*/
digest(hashInto: NodeJS.TypedArray): NodeJS.TypedArray;

Expand Down
23 changes: 0 additions & 23 deletions src/boringssl_sys/boringssl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -823,29 +823,6 @@ pub const SSL_GROUP_SECP521R1: u16 = 25;
pub const SSL_GROUP_X25519: u16 = 29;
pub const SSL_GROUP_X448: u16 = 30;

/// `sk_X509_pop_free(sk, X509_free)` — release a `STACK_OF(X509)` and every
/// element on it.
#[inline]
pub unsafe fn sk_X509_pop_free(sk: *mut struct_stack_st_X509) {
unsafe extern "C" fn call(f: OPENSSL_sk_free_func, e: *mut c_void) {
// SAFETY: BoringSSL only invokes this with the `free_func` we passed
// and a live element pointer.
unsafe { (f.unwrap())(e) }
}
// SAFETY: caller-guaranteed live X509 stack; `X509_free` is
// `extern "C" fn(*mut X509)`, ABI-compatible with `fn(*mut c_void)`.
unsafe {
sk_pop_free_ex(
sk.cast(),
Some(call),
Some(core::mem::transmute::<
unsafe extern "C" fn(*mut X509),
unsafe extern "C" fn(*mut c_void),
>(X509_free)),
);
}
}

#[inline]
pub unsafe fn sk_X509_value(sk: *const struct_stack_st_X509, i: usize) -> *mut X509 {
// SAFETY: Two independent type casts, not a const→mut provenance laundering:
Expand Down
2 changes: 1 addition & 1 deletion src/codegen/builtins/host/features-bun.builtins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ export default b.declarations(
extendAll(
["Sockets"],
"%Listener%",
...nativeBindings("Bindgen", "socket_body.bind.ts", "getBufferedAmount", "isNamedPipeSocket"),
...nativeBindings("Bindgen", "socket_body.bind.ts", "getBufferedAmount", "hasPendingRead", "isNamedPipeSocket"),
),

extendAll(
Expand Down
25 changes: 24 additions & 1 deletion src/js/ahead-of-time/node/_http_agent.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// What the compiler reads in place of src/js/node/_http_agent.ts, which `bun run` evaluates. The two do the same.
// Written against that file with the SHA-256 c7494682bc21dc2e3ec7e56d4de7e66688ab4289f0007044a358f32e5d1b4cc8.
// Written against that file with the SHA-256 9161c607d9c8f981ce072e148aa617473a8be3f076599f05727c3c23a97b8aff.
// When that file changes, bring the change here and write the new hash: test/internal/second-texts-of-modules.test.ts.

// This is a port of Node.js's lib/_http_agent.js
Expand All @@ -18,6 +18,7 @@ const { validateNumber, validateOneOf, validateString } = require("internal/vali
const { isIP } = require("internal/net/isIP");
const netSymbols = require("internal/net/symbols");
const kDestroyOnRead: typeof netSymbols.kDestroyOnRead = netSymbols.kDestroyOnRead;
const handleHasPendingRead: (handle: unknown) => boolean = $bindgen("socket_body.bind.ts", "hasPendingRead");

const kOnKeylog = Symbol("onkeylog");
const kRequestOptions = Symbol("requestOptions");
Expand All @@ -30,6 +31,16 @@ const kOwnerFrame = Symbol("ownerFrame");
const AsyncContextFrame = require("internal/async_context_frame");
const ObjectDefineProperty = Object.defineProperty;

// kDestroyOnRead only acts when the event loop polls for I/O. Data that
// reached the OS socket buffer after the last poll has not been seen by it
// yet, so check for it directly before handing an idle socket to a request.
function hasPendingRead(socket: $Node.HttpClientSocket) {
// A socket that is still connecting has nothing to read yet, and a pending
// connect error must be reported to the request rather than swallowed here.
if (socket.connecting) return false;
return handleHasPendingRead(socket._handle);
}

function freeSocketErrorListener(this: $Node.HttpClientSocket, err: Error) {
const socket = this;
$debug("SOCKET ERROR on FREE socket:", err.message, err.stack);
Expand Down Expand Up @@ -101,6 +112,7 @@ declare global {
}

interface HttpClientSocket {
_handle?: unknown;
[kDestroyOnRead]?: boolean;
}
}
Expand Down Expand Up @@ -178,6 +190,12 @@ function Agent(this: $Node.Agent, options?: $Node.AgentOptions): void {

const requests = this.requests[name];
if (requests?.length) {
if (hasPendingRead(socket)) {
// onClose() will create a new socket for the pending request.
$debug("PENDING DATA on released socket - destroying");
socket.destroy();
return;
}
const req = requests.shift()!;
const reqAsyncRes = req[kRequestAsyncResource];
if (reqAsyncRes) {
Expand Down Expand Up @@ -335,6 +353,11 @@ Agent.prototype.addRequest = function addRequest(
freeSockets.shift();
}
socket = this.scheduling === "fifo" ? freeSockets.shift() : freeSockets.pop();
while (socket && hasPendingRead(socket)) {
$debug("PENDING DATA on FREE socket - destroying");
socket.destroy();
socket = this.scheduling === "fifo" ? freeSockets.shift() : freeSockets.pop();
}
if (!freeSockets.length) delete this.freeSockets[name];
}

Expand Down
1 change: 1 addition & 0 deletions src/js/builtins.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,7 @@ declare function $ERR_HTTP2_STATUS_INVALID(code: number): RangeError;
declare function $ERR_HTTP2_INVALID_PSEUDOHEADER(name: string): TypeError;
declare function $ERR_HTTP2_STREAM_ERROR(code): Error;
declare function $ERR_HTTP2_SESSION_ERROR(code): Error;
declare function $ERR_HTTP2_TOO_MANY_ORIGINS(max: number): Error;
declare function $ERR_HTTP2_PAYLOAD_FORBIDDEN(status): Error;
declare function $ERR_HTTP2_INVALID_INFO_STATUS(code): RangeError;
declare function $ERR_INVALID_URL(input, base?): TypeError;
Expand Down
1 change: 1 addition & 0 deletions src/js/internal/streams/readable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,7 @@ Readable.prototype.setEncoding = function (this: $Node.Readable, enc: string) {
content += decoder.write(data);
}
}
if ((state[kState] & Flag.Ended) !== 0) content += decoder.end();
state.buffer.length = 0;
state.bufferIndex = 0;

Expand Down
22 changes: 22 additions & 0 deletions src/js/node/_http_agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const { getLazy, kEmptyObject, once } = require("internal/shared");
const { validateNumber, validateOneOf, validateString } = require("internal/validators");
const { isIP } = require("internal/net/isIP");
const { kDestroyOnRead } = require("internal/net/symbols");
const handleHasPendingRead: (handle: unknown) => boolean = $bindgen("socket_body.bind.ts", "hasPendingRead");

const kOnKeylog = Symbol("onkeylog");
const kRequestOptions = Symbol("requestOptions");
Expand All @@ -24,6 +25,16 @@ const kOwnerFrame = Symbol("ownerFrame");
const AsyncContextFrame = require("internal/async_context_frame");
const ObjectDefineProperty = Object.defineProperty;

// kDestroyOnRead only acts when the event loop polls for I/O. Data that
// reached the OS socket buffer after the last poll has not been seen by it
// yet, so check for it directly before handing an idle socket to a request.
function hasPendingRead(socket) {
// A socket that is still connecting has nothing to read yet, and a pending
// connect error must be reported to the request rather than swallowed here.
if (socket.connecting) return false;
return handleHasPendingRead(socket._handle);
}

function freeSocketErrorListener(err) {
const socket = this;
$debug("SOCKET ERROR on FREE socket:", err.message, err.stack);
Expand Down Expand Up @@ -104,6 +115,12 @@ function Agent(options): void {

const requests = this.requests[name];
if (requests?.length) {
if (hasPendingRead(socket)) {
// onClose() will create a new socket for the pending request.
$debug("PENDING DATA on released socket - destroying");
socket.destroy();
return;
Comment thread
dylan-conway marked this conversation as resolved.
Resolved
}
const req = requests.shift();
const reqAsyncRes = req[kRequestAsyncResource];
if (reqAsyncRes) {
Expand Down Expand Up @@ -261,6 +278,11 @@ Agent.prototype.addRequest = function addRequest(req, options, port /* legacy */
freeSockets.shift();
}
socket = this.scheduling === "fifo" ? freeSockets.shift() : freeSockets.pop();
while (socket && hasPendingRead(socket)) {
$debug("PENDING DATA on FREE socket - destroying");
socket.destroy();
socket = this.scheduling === "fifo" ? freeSockets.shift() : freeSockets.pop();
}
Comment thread
dylan-conway marked this conversation as resolved.
Resolved
Comment thread
dylan-conway marked this conversation as resolved.
Resolved
if (!freeSockets.length) delete this.freeSockets[name];
}

Expand Down
8 changes: 4 additions & 4 deletions src/js/node/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ function exportChallenge(spkac: string | BufferSource, encoding?: string) {
}

function Certificate(): void {
if (!new.target) {
if (!(this instanceof Certificate)) {
return new Certificate() as any;
}

Expand Down Expand Up @@ -430,7 +430,7 @@ crypto_exports.createHash = function createHash(algorithm: string, options?: $No
};

function Hmac(this: $Node.Hmac, hmac: string, key: unknown, options?: $Node.TransformOptions): void {
if (!new.target) {
if (!(this instanceof Hmac)) {
return new Hmac(hmac, key, options) as any;
}
Comment thread
dylan-conway marked this conversation as resolved.
Resolved

Expand Down Expand Up @@ -667,7 +667,7 @@ crypto_exports.createECDH = function createECDH(curve: string) {
iv: unknown,
options?: $Node.TransformOptions,
): void {
if (!new.target) {
if (!(this instanceof Cipheriv)) {
Comment thread
dylan-conway marked this conversation as resolved.
Resolved
return new Cipheriv(cipher, key, iv, options) as any;
}

Expand Down Expand Up @@ -735,7 +735,7 @@ crypto_exports.createECDH = function createECDH(curve: string) {
iv: unknown,
options?: $Node.TransformOptions,
): void {
if (!new.target) {
if (!(this instanceof Decipheriv)) {
return new Decipheriv(cipher, key, iv, options) as any;
}

Expand Down
24 changes: 16 additions & 8 deletions src/js/node/http2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ type Http2ConnectOptions = {
maxConcurrentStreams?: number;
maxOutstandingSettings?: number;
maxReservedRemoteStreams?: number;
maxOriginSetSize?: number;
remoteCustomSettings?: number[];
strictFieldWhitespaceValidation?: boolean;
strictSingleValueFields?: boolean;
Expand Down Expand Up @@ -5199,6 +5200,7 @@ class ClientHttp2Session extends Http2Session {
// RFC 9113 reserved (pushed) streams the peer may have open at once (node session option).
#maxReservedRemoteStreams: number = 200;
#reservedStreamsCount: number = 0;
#maxOriginSetSize: number = 128;
#strictFieldWhitespaceValidation: boolean = true;
// Client-side SETTINGS_MAX_CONCURRENT_STREAMS accounting: requests whose HEADERS frame has been
// submitted and whose stream has not closed yet, plus the queue of requests waiting for a slot
Expand Down Expand Up @@ -5563,17 +5565,17 @@ class ClientHttp2Session extends Http2Session {
origin(self: ClientHttp2Session, origin: string | Array<string> | undefined) {
if (!self) return;
const socket = self[bunHTTP2Socket];
if (self.encrypted && socket) {
if (self.encrypted && socket && origin) {
const originSet = initOriginSet(self, socket);
if ($isArray(origin)) {
for (const item of origin) {
originSet.add(item);
const origins = $isArray(origin) ? origin : [origin];
for (const item of origins) {
if (originSet.size >= self.#maxOriginSetSize) {
self.destroy($ERR_HTTP2_TOO_MANY_ORIGINS(self.#maxOriginSetSize));
return;
}
Comment thread
dylan-conway marked this conversation as resolved.
Resolved
self.emit("origin", origin);
} else if (origin) {
originSet.add(origin);
self.emit("origin", [origin]);
originSet.add(item);
}
self.emit("origin", origins);
}
},
write(self: ClientHttp2Session, buffer: Buffer, transportClosed: boolean) {
Expand Down Expand Up @@ -5848,6 +5850,12 @@ class ClientHttp2Session extends Http2Session {
assertIsObject(options, "options");
options = { ...options };

const { maxOriginSetSize } = options;
if (maxOriginSetSize != null) {
validateNumber(maxOriginSetSize, "options.maxOriginSetSize", 0);
this.#maxOriginSetSize = maxOriginSetSize;
}

assertIsArray(options.remoteCustomSettings, "options.remoteCustomSettings");
if (options.remoteCustomSettings) {
options.remoteCustomSettings = [...options.remoteCustomSettings];
Expand Down
27 changes: 17 additions & 10 deletions src/js/node/net.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ declare global {
[kerrorEmitted]?: boolean;

[kTimeout]: Timer | null;
[kLastWriteQueueSize]: number;
[kDestroyOnRead]: boolean;
[kupgraded]: Socket | null;
[kOnUpgradedClose]: (() => void) | undefined;
Expand Down Expand Up @@ -700,6 +701,7 @@ const kOnreadPendingEnd = Symbol("kOnreadPendingEnd");
const kOnreadReadRequested = Symbol("kOnreadReadRequested");
const kOnreadEmptyTail = Buffer.alloc(0);
const kwriteCallback = Symbol("writeCallback");
const kLastWriteQueueSize = Symbol("lastWriteQueueSize");
const kRejectedWrite = Symbol("kRejectedWrite");
const kSocketClass = Symbol("kSocketClass");

Expand Down Expand Up @@ -1223,6 +1225,8 @@ const SocketHandlers = {
if (!holdRejectedWrite(self, socket, res, callback)) failWrite(self, res, callback);
} else if (res) {
self._pendingData = self[kwriteCallback] = null;
self._unrefTimer();
self[kLastWriteQueueSize] = 0;
unrefAfterDrain(self, socket);
callback(null);
} else {
Expand Down Expand Up @@ -2083,6 +2087,8 @@ const SocketHandlers2 = {
} else if (res) {
self[kBytesWritten] = socket.bytesWritten;
self._pendingData = self[kwriteCallback] = null;
self._unrefTimer();
self[kLastWriteQueueSize] = 0;
unrefAfterDrain(self, socket);
callback(null);
} else {
Expand Down Expand Up @@ -2390,6 +2396,7 @@ function Socket(this: $Node.Socket, options?: $Node.SocketOptions): void {
this._port = undefined;
this[bunTLSConnectOptions] = null;
this.timeout = 0;
this[kLastWriteQueueSize] = 0;
// node initializes the timer slot to null so it is observable before setTimeout() is called.
// https://github.com/nodejs/node/blob/v26.3.0/lib/net.js#L401
this[kTimeout] = null;
Expand Down Expand Up @@ -2591,17 +2598,16 @@ Socket.prototype.address = function address(this: $Node.Socket) {
};

Socket.prototype._onTimeout = function (this: $Node.Socket) {
// if there is pending data, write is in progress
// so we suppress the timeout
if (this._pendingData) {
return;
}

const handle = this._handle;
// if there is a handle, and it has pending data,
// we suppress the timeout because a write is in progress
if (handle && getBufferedAmount(handle) > 0) {
return;
if (handle) {
// As in Node, a write that moved since the last look puts the timeout off. Its first look is at the size of the write, this one at 0.
// https://github.com/nodejs/node/blob/v26.12.1/lib/net.js#L872-L887
const writeQueueSize = getBufferedAmount(handle);
if (writeQueueSize !== this[kLastWriteQueueSize]) {
Comment thread
dylan-conway marked this conversation as resolved.
Resolved
this[kLastWriteQueueSize] = writeQueueSize;
this._unrefTimer();
Comment thread
dylan-conway marked this conversation as resolved.
Resolved
Comment thread
dylan-conway marked this conversation as resolved.
Resolved
return;
}
}
this.emit("timeout");
};
Comment thread
dylan-conway marked this conversation as resolved.
Resolved
Expand Down Expand Up @@ -3464,6 +3470,7 @@ Socket.prototype._write = function _write(
return false;
}
this._unrefTimer();
this[kLastWriteQueueSize] = 0;
if (this.secureConnecting) this[kPreHandshakeWrite] = true;
if (socket.readyState < 0) {
// The handle's native socket was already closed (e.g. handle.close() was
Expand Down
1 change: 1 addition & 0 deletions src/jsc/bindings/ErrorCode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2144,6 +2144,7 @@ static constexpr SimpleErrorMessage simpleErrorMessages[] = {
{ ErrorCode::ERR_HTTP2_INVALID_PSEUDOHEADER, 1, { "\""_s, "\" is an invalid pseudoheader or is used incorrectly"_s, ""_s } },
{ ErrorCode::ERR_HTTP2_STREAM_ERROR, 1, { "Stream closed with error code "_s, ""_s, ""_s } },
{ ErrorCode::ERR_HTTP2_SESSION_ERROR, 1, { "Session closed with error code "_s, ""_s, ""_s } },
{ ErrorCode::ERR_HTTP2_TOO_MANY_ORIGINS, 1, { "The server sent more ORIGIN frames than the allowed number of "_s, ""_s, ""_s } },
{ ErrorCode::ERR_HTTP2_PAYLOAD_FORBIDDEN, 1, { "Responses with "_s, " status must not have a payload"_s, ""_s } },
{ ErrorCode::ERR_HTTP2_INVALID_INFO_STATUS, 1, { "Invalid informational status code: "_s, ""_s, ""_s } },
{ ErrorCode::ERR_HTTP_INVALID_HEADER_VALUE, 2, { "Invalid value \""_s, "\" for header \""_s, "\""_s } },
Expand Down
1 change: 1 addition & 0 deletions src/jsc/bindings/ErrorCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,5 +359,6 @@ const errors: ErrorCodeMapping = [
["ERR_REDIS_SERVER_ERROR", Error, "RedisError"],
["ERR_FFI_CC_DISABLED", Error],
["HPE_INVALID_URL", Error],
["ERR_HTTP2_TOO_MANY_ORIGINS", Error],
];
export default errors;
4 changes: 2 additions & 2 deletions src/jsc/bindings/node/crypto/CryptoUtil.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ namespace Bun {
using namespace JSC;
using namespace ncrypto;

int pqcKeyTypeToNid(const WTF::StringView& name, bool ignoreCase)
int pqcKeyTypeToNid(const WTF::StringView& name)
{
static constexpr std::pair<ASCIILiteral, int> table[] = {
{ "ml-dsa-44"_s, EVP_PKEY_ML_DSA_44 },
Expand All @@ -29,7 +29,7 @@ int pqcKeyTypeToNid(const WTF::StringView& name, bool ignoreCase)
{ "ml-kem-1024"_s, EVP_PKEY_ML_KEM_1024 },
};
for (const auto& [candidate, nid] : table) {
if (ignoreCase ? WTF::equalIgnoringASCIICase(name, candidate) : name == candidate)
if (name == candidate)
return nid;
}
return 0;
Expand Down
2 changes: 1 addition & 1 deletion src/jsc/bindings/node/crypto/CryptoUtil.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ enum class DSASigEnc {
// ML-DSA / ML-KEM parameter sets, as provided by BoringSSL's EVP_PKEY
// integration. BoringSSL has ML-KEM-768 and ML-KEM-1024 but not ML-KEM-512.
// Each returns 0 / a null literal / nullptr for any other key type.
int pqcKeyTypeToNid(const WTF::StringView& name, bool ignoreCase = false);
int pqcKeyTypeToNid(const WTF::StringView& name);
ASCIILiteral pqcNidToKeyTypeName(int nid);
bool isMlDsaNid(int nid);
bool isMlKemNid(int nid);
Expand Down
3 changes: 2 additions & 1 deletion src/jsc/bindings/node/crypto/JSCipher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ JSValue rsaFunction(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* ca
if (JSObject* options = optionsValue.getObject()) {
JSValue paddingValue = options->get(lexicalGlobalObject, Identifier::fromString(vm, "padding"_s));
RETURN_IF_EXCEPTION(scope, {});
if (!paddingValue.isUndefined()) {
// Node: `key.padding || defaultPadding`
if (paddingValue.toBoolean(lexicalGlobalObject)) {
padding = paddingValue.toInt32(lexicalGlobalObject);
RETURN_IF_EXCEPTION(scope, {});
}
Expand Down
Loading
Loading