Input validation / Memory safety

MEDIUM
nodejs/node
Commit: 74c365846533
Affected: <=25.9.0
2026-04-05 11:26 UTC

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' + })); } {
← Back to Alerts View on GitHub →