Input validation / Memory safety
Description
The commit hardens input validation for the internal stack-calling utility by enabling V8 internal checks (-DV8_ENABLE_CHECKS) and constraining frameCount to be an integer within [1, 200]. It changes: (1) lib/util.js to use validateInteger for frameCount instead of validateNumber; (2) GetCallSites path in src/node_util.cc to require an unsigned 32-bit frameCount value (IsUint32) and enforce the range; (3) tests to require integer frameCount (non-integers like 3.6 now throw ERR_OUT_OF_RANGE); (4) minor type-correctness fixes in several files to align with strict input handling. The net effect is to prevent non-integer or out-of-range inputs from being treated as valid, which reduces the risk of memory-safety issues or crashes due to invalid stack-capture inputs. This is a hardening of input validation and runtime checks rather than a feature addition. It addresses security-relevant input validation and memory-safety concerns.
Commit Details
Author: Ryuhei Shima
Date: 2026-01-28 04:17 UTC
Message:
build: enable -DV8_ENABLE_CHECKS flag
Fixes: https://github.com/nodejs/node/issues/61301
PR-URL: https://github.com/nodejs/node/pull/61327
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
Triage Assessment
Vulnerability Type: Input validation / Memory safety
Confidence: MEDIUM
Reasoning:
The commit enables V8 internal checks and tightens input validation (e.g., frameCount must be an integer and within a valid range). These changes harden runtime behavior, reducing risk of invalid inputs causing crashes or potential vulnerabilities. While not a direct feature/content fix, it addresses security-relevant input validation and memory-safety concerns.
Verification Assessment
Vulnerability Type: Input validation / Memory safety
Confidence: MEDIUM
Affected Versions: <=25.9.0
Code Diff
diff --git a/configure.py b/configure.py
index 27b827b8ed3e4b..a37ebefe7c0507 100755
--- a/configure.py
+++ b/configure.py
@@ -1969,7 +1969,7 @@ def configure_library(lib, output, pkgname=None):
def configure_v8(o, configs):
- set_configuration_variable(configs, 'v8_enable_v8_checks', release=1, debug=0)
+ set_configuration_variable(configs, 'v8_enable_v8_checks', release=0, debug=1)
o['variables']['v8_enable_webassembly'] = 0 if options.v8_lite_mode else 1
o['variables']['v8_enable_javascript_promise_hooks'] = 1
@@ -2600,11 +2600,10 @@ def make_bin_override():
del configurations['Release']['variables']
config_debug_vars = configurations['Debug']['variables']
del configurations['Debug']['variables']
-output['conditions'].append(['build_type=="Release"', {
- 'variables': config_release_vars,
-}, {
- 'variables': config_debug_vars,
-}])
+if options.debug:
+ variables = variables | config_debug_vars
+else:
+ variables = variables | config_release_vars
# make_global_settings should be a root level element too
if 'make_global_settings' in output:
diff --git a/lib/util.js b/lib/util.js
index ce9452e8715a68..ebd486addc7eb2 100644
--- a/lib/util.js
+++ b/lib/util.js
@@ -66,6 +66,7 @@ const {
validateString,
validateOneOf,
validateObject,
+ validateInteger,
} = require('internal/validators');
const {
isReadableStream,
@@ -452,7 +453,7 @@ function getCallSites(frameCount = 10, options) {
}
// Using kDefaultMaxCallStackSizeToCapture as reference
- validateNumber(frameCount, 'frameCount', 1, 200);
+ validateInteger(frameCount, 'frameCount', 1, 200);
// If options.sourceMaps is true or if sourceMaps are enabled but the option.sourceMaps is not set explicitly to false
if (options.sourceMap === true || (getOptionValue('--enable-source-maps') && options.sourceMap !== false)) {
return mapCallSite(binding.getCallSites(frameCount));
diff --git a/src/heap_utils.cc b/src/heap_utils.cc
index 4708e57e2bb9bb..b64e43eeccb629 100644
--- a/src/heap_utils.cc
+++ b/src/heap_utils.cc
@@ -89,7 +89,7 @@ class JSGraph : public EmbedderGraph {
}
Node* V8Node(const Local<v8::Value>& value) override {
- return V8Node(value.As<v8::Data>());
+ return V8Node(v8::Local<v8::Data>(value));
}
Node* AddNode(std::unique_ptr<Node> node) override {
diff --git a/src/node_builtins.cc b/src/node_builtins.cc
index db83c46c81f767..2e7b50511f24c3 100644
--- a/src/node_builtins.cc
+++ b/src/node_builtins.cc
@@ -9,6 +9,7 @@
#include "quic/guard.h"
#include "simdutf.h"
#include "util-inl.h"
+#include "v8-value.h"
namespace node {
namespace builtins {
@@ -441,7 +442,7 @@ void BuiltinLoader::SaveCodeCache(const std::string& id, Local<Data> data) {
new_cached_data.reset(
ScriptCompiler::CreateCodeCache(mod->GetUnboundModuleScript()));
} else {
- Local<Function> fun = data.As<Function>();
+ Local<Function> fun = data.As<Value>().As<Function>();
new_cached_data.reset(ScriptCompiler::CreateCodeCacheForFunction(fun));
}
CHECK_NOT_NULL(new_cached_data);
diff --git a/src/node_util.cc b/src/node_util.cc
index af42a3bd72c3f4..fbfda9c1551e07 100644
--- a/src/node_util.cc
+++ b/src/node_util.cc
@@ -258,7 +258,7 @@ static void GetCallSites(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(context);
CHECK_EQ(args.Length(), 1);
- CHECK(args[0]->IsNumber());
+ CHECK(args[0]->IsUint32());
const uint32_t frames = args[0].As<Uint32>()->Value();
CHECK(frames >= 1 && frames <= 200);
diff --git a/src/util-inl.h b/src/util-inl.h
index 6898e8ea794675..f8cccfef6b65b3 100644
--- a/src/util-inl.h
+++ b/src/util-inl.h
@@ -22,6 +22,7 @@
#ifndef SRC_UTIL_INL_H_
#define SRC_UTIL_INL_H_
+#include "v8-isolate.h"
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#include <cmath>
@@ -678,10 +679,15 @@ T FromV8Value(v8::Local<v8::Value> value) {
"Type is out of unsigned integer range");
if constexpr (!loose) {
CHECK(value->IsUint32());
+ return static_cast<T>(value.As<v8::Uint32>()->Value());
} else {
CHECK(value->IsNumber());
+ v8::Isolate* isolate = v8::Isolate::GetCurrent();
+ v8::Local<v8::Context> context = isolate->GetCurrentContext();
+ v8::Maybe<uint32_t> maybe = value->Uint32Value(context);
+ CHECK(!maybe.IsNothing());
+ return static_cast<T>(maybe.FromJust());
}
- return static_cast<T>(value.As<v8::Uint32>()->Value());
} else if constexpr (std::is_integral_v<T> && std::is_signed_v<T>) {
static_assert(
std::numeric_limits<T>::max() <= std::numeric_limits<int32_t>::max() &&
diff --git a/test/parallel/test-util-getcallsites.js b/test/parallel/test-util-getcallsites.js
index 7cab4f6cac6397..a14fcb3482cdf9 100644
--- a/test/parallel/test-util-getcallsites.js
+++ b/test/parallel/test-util-getcallsites.js
@@ -31,15 +31,14 @@ const assert = require('node:assert');
);
}
-// Guarantee dot-right numbers are ignored
+// frameCount must be an integer
{
- const callSites = getCallSites(3.6);
- assert.strictEqual(callSites.length, 3);
-}
-
-{
- const callSites = getCallSites(3.4);
- assert.strictEqual(callSites.length, 3);
+ assert.throws(() => {
+ const callSites = getCallSites(3.6);
+ assert.strictEqual(callSites.length, 3);
+ }, common.expectsError({
+ code: 'ERR_OUT_OF_RANGE'
+ }));
}
{