Commit bc6e1ad8f9a for nodejs
commit bc6e1ad8f9aa620bc20b8771509d1fcb26d7ee23
Author: Yagiz Nizipli <yagiz@nizipli.com>
Date: Fri Sep 25 10:39:01 2026 -0400
net: speed up BlockList.check for strings
Skip toLowerCase() for the documented ipv4/ipv6 families and add a
V8 Fast API for one-byte address strings. The FastOneByteString
callback takes FastApiCallbackOptions so V8 can use the fast path
reliably (required for string-shaped Fast API arguments).
Overlong IPv6 zone ids are parsed like uv_ip6_addr (unknown zone
is scope_id 0; address parts 40+ chars stay a miss). IPv4 zone
suffixes stay a miss, matching uv_ip4_addr.
Assisted-by: a closed-source coding agent
Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
PR-URL: https://github.com/nodejs/node/pull/66166
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Tim Perry <pimterry@gmail.com>
diff --git a/lib/internal/blocklist.js b/lib/internal/blocklist.js
index 9436078eaa2..b4d6fe90512 100644
--- a/lib/internal/blocklist.js
+++ b/lib/internal/blocklist.js
@@ -315,8 +315,17 @@ class BlockList {
validateString(family, 'family');
// Fast path: pass the string directly to C++ which does
// inet_pton + Apply() without allocating a JS SocketAddress wrapper.
- const af = StringPrototypeToLowerCase(family) === 'ipv4' ?
- AF_INET : AF_INET6;
+ // The documented values are 'ipv4' / 'ipv6' (and the IPvX aliases).
+ // Avoid toLowerCase on that hot path.
+ let af;
+ if (family === 'ipv4' || family === 'IPv4') {
+ af = AF_INET;
+ } else if (family === 'ipv6' || family === 'IPv6') {
+ af = AF_INET6;
+ } else {
+ af = StringPrototypeToLowerCase(family) === 'ipv4' ?
+ AF_INET : AF_INET6;
+ }
return this[kHandle].checkString(address, af);
}
return Boolean(this[kHandle].check(address[kSocketAddressHandle]));
diff --git a/src/node_sockaddr.cc b/src/node_sockaddr.cc
index 562bb614600..3f903f548e1 100644
--- a/src/node_sockaddr.cc
+++ b/src/node_sockaddr.cc
@@ -8,6 +8,9 @@
#include "node_hash.h"
#include "node_sockaddr-inl.h" // NOLINT(build/include_inline)
#include "uv.h"
+#include "v8-fast-api-calls.h"
+
+#include <cstring>
#include <memory>
#include <string>
@@ -18,6 +21,8 @@ namespace node {
using v8::Array;
using v8::CFunction;
using v8::Context;
+using v8::FastApiCallbackOptions;
+using v8::FastOneByteString;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::Int32;
@@ -1032,6 +1037,53 @@ void SocketAddressBlockListWrap::CheckString(
args.GetReturnValue().Set(wrap->blocklist_->Apply(addr));
}
+bool SocketAddressBlockListWrap::FastCheckString(
+ Local<Object> receiver,
+ const FastOneByteString& address,
+ int32_t family,
+ // NOLINTNEXTLINE(runtime/references) This is V8 api.
+ FastApiCallbackOptions& options) {
+ // FastOneByteString is not NUL-terminated. Copy onto the stack
+ // before any other work: a GC would invalidate `address`.
+ //
+ // uv_ip6_addr only needs the address part (≤39 chars) plus an
+ // optional %zone. A zone longer than UV_IF_NAMESIZE is unknown
+ // (scope_id 0), same as omitting it. Keep that behavior when the
+ // full string does not fit so Fast API and CheckString agree.
+ constexpr size_t kMax = INET6_ADDRSTRLEN + UV_IF_NAMESIZE;
+ char buf[kMax];
+ if (address.length < kMax) {
+ memcpy(buf, address.data, address.length);
+ buf[address.length] = '\0';
+ } else if (family == AF_INET6) {
+ // uv_ip4_addr rejects %zone. Only IPv6 may drop an overlong zone
+ // (unknown zone → scope_id 0, same as uv_ip6_addr).
+ // uv_ip6_addr copies the address part into a 40-byte buffer
+ // (39 chars + NUL). A 40+ char prefix (mixed notation) is
+ // truncated and fails inet_pton — do not keep the full prefix.
+ const char* percent =
+ static_cast<const char*>(memchr(address.data, '%', address.length));
+ if (percent == nullptr) return false;
+ const size_t addr_len = static_cast<size_t>(percent - address.data);
+ if (addr_len >= 40) return false;
+ memcpy(buf, address.data, addr_len);
+ buf[addr_len] = '\0';
+ } else {
+ return false;
+ }
+ USE(options);
+
+ TRACK_V8_FAST_API_CALL("blocklist.checkString");
+ SocketAddressBlockListWrap* wrap =
+ FromJSObject<SocketAddressBlockListWrap>(receiver);
+ SocketAddress addr;
+ if (!SocketAddress::New(family, buf, 0, &addr)) return false;
+ return wrap->blocklist_->Apply(addr);
+}
+
+CFunction SocketAddressBlockListWrap::fast_check_string_(
+ CFunction::Make(&SocketAddressBlockListWrap::FastCheckString));
+
void SocketAddressBlockListWrap::GetRules(
const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
@@ -1087,7 +1139,11 @@ Local<FunctionTemplate> SocketAddressBlockListWrap::GetConstructorTemplate(
SetProtoMethod(isolate, tmpl, "removeSubnet", RemoveSubnet);
SetFastMethod(
isolate, tmpl->PrototypeTemplate(), "check", Check, &fast_check_);
- SetProtoMethod(isolate, tmpl, "checkString", CheckString);
+ SetFastMethod(isolate,
+ tmpl->PrototypeTemplate(),
+ "checkString",
+ CheckString,
+ &fast_check_string_);
SetProtoMethod(isolate, tmpl, "getRules", GetRules);
SetProtoMethodNoSideEffect(isolate, tmpl, "getSize", GetSize);
SetProtoMethod(isolate, tmpl, "clear", Clear);
diff --git a/src/node_sockaddr.h b/src/node_sockaddr.h
index ef4155110db..dccfc798fa4 100644
--- a/src/node_sockaddr.h
+++ b/src/node_sockaddr.h
@@ -9,6 +9,7 @@
#include "node.h"
#include "node_worker.h"
#include "uv.h"
+#include "v8-fast-api-calls.h"
#include "v8.h"
#include <compare>
@@ -418,6 +419,12 @@ class SocketAddressBlockListWrap : public BaseObject {
static bool FastCheck(v8::Local<v8::Object> receiver,
v8::Local<v8::Object> addr_obj);
static void CheckString(const v8::FunctionCallbackInfo<v8::Value>& args);
+ static bool FastCheckString(
+ v8::Local<v8::Object> receiver,
+ const v8::FastOneByteString& address,
+ int32_t family,
+ // NOLINTNEXTLINE(runtime/references) This is V8 api.
+ v8::FastApiCallbackOptions& options);
static void GetRules(const v8::FunctionCallbackInfo<v8::Value>& args);
static void GetSize(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Clear(const v8::FunctionCallbackInfo<v8::Value>& args);
@@ -465,6 +472,7 @@ class SocketAddressBlockListWrap : public BaseObject {
private:
std::shared_ptr<SocketAddressBlockList> blocklist_;
static v8::CFunction fast_check_;
+ static v8::CFunction fast_check_string_;
};
} // namespace node
diff --git a/test/parallel/test-blocklist-fast-api.js b/test/parallel/test-blocklist-fast-api.js
index 3c59ad361d1..c0d9db2d4c7 100644
--- a/test/parallel/test-blocklist-fast-api.js
+++ b/test/parallel/test-blocklist-fast-api.js
@@ -6,15 +6,14 @@ const assert = require('assert');
const { BlockList } = require('net');
const { internalBinding } = require('internal/test/binding');
-// The fast API is on the native check() method which takes a
-// SocketAddressBase object. The JS BlockList.prototype.check() routes
-// string arguments to checkString() which has no fast API, so we need
-// to use SocketAddress objects to exercise the fast API path.
+// The native check() method takes a SocketAddressBase object.
+// String arguments go through checkString(), which also has a fast API.
const { kHandle: kBlockListHandle } = require('internal/blocklist');
const {
SocketAddress,
kHandle: kSocketAddressHandle,
} = require('internal/socketaddress');
+const { AF_INET } = internalBinding('block_list');
const blockList = new BlockList();
blockList.addAddress('1.1.1.1');
@@ -31,12 +30,24 @@ function testFastCheck() {
assert.strictEqual(handle.check(addr3), true);
}
+function checkString(address) {
+ return handle.checkString(address, AF_INET);
+}
+
eval('%PrepareFunctionForOptimization(testFastCheck)');
testFastCheck();
eval('%OptimizeFunctionOnNextCall(testFastCheck)');
testFastCheck();
+eval('%PrepareFunctionForOptimization(checkString)');
+assert.strictEqual(checkString('1.1.1.1'), true);
+eval('%OptimizeFunctionOnNextCall(checkString)');
+assert.strictEqual(checkString('1.1.1.1'), true);
+assert.strictEqual(checkString('2.2.2.2'), false);
+assert.strictEqual(checkString('10.0.0.5'), true);
+
if (common.isDebug) {
const { getV8FastApiCallCount } = internalBinding('debug');
assert.strictEqual(getV8FastApiCallCount('blocklist.check'), 3);
+ assert.strictEqual(getV8FastApiCallCount('blocklist.checkString'), 3);
}
diff --git a/test/parallel/test-blocklist.js b/test/parallel/test-blocklist.js
index f1fc42f6304..b0fe4303bfb 100644
--- a/test/parallel/test-blocklist.js
+++ b/test/parallel/test-blocklist.js
@@ -868,6 +868,29 @@ const util = require('util');
assert.strictEqual(blockList.check('not_valid_ipv6', 'ipv6'), false);
}
+// uv_ip6_addr ignores an unknown/overlong zone (scope_id 0). A Fast API
+// stack limit must not turn that into a miss after JIT.
+{
+ const blockList = new BlockList();
+ blockList.addAddress('fe80::1', 'ipv6');
+ const longZone = `fe80::1%${'z'.repeat(200)}`;
+ assert.strictEqual(blockList.check('fe80::1', 'ipv6'), true);
+ assert.strictEqual(blockList.check(longZone, 'ipv6'), true);
+ assert.strictEqual(blockList.check('x'.repeat(200), 'ipv6'), false);
+
+ // uv_ip4_addr rejects zone suffixes. The Fast API overflow path
+ // must not strip %zone and treat this as 1.1.1.1.
+ blockList.addAddress('1.1.1.1');
+ assert.strictEqual(blockList.check(`1.1.1.1%${'z'.repeat(200)}`), false);
+
+ // Mixed-notation IPv6 is 45 chars. uv_ip6_addr truncates the
+ // address part to 39 when a zone is present, so this is a miss.
+ const mixed = 'ffff:ffff:ffff:ffff:ffff:ffff:255.255.255.255';
+ blockList.addAddress(mixed, 'ipv6');
+ assert.strictEqual(blockList.check(mixed, 'ipv6'), true);
+ assert.strictEqual(blockList.check(`${mixed}%${'z'.repeat(200)}`, 'ipv6'), false);
+}
+
// check() family parameter is case-insensitive.
{
const blockList = new BlockList();