Skip to content

Commit 99457c0

Browse files
bkaradzic-microsoftbkaradzicCopilot
authored
Fix integer overflow in Chakra napi_create_dataview bounds check (#181)
## Summary `napi_create_dataview` (Chakra backend) validated the requested view with an unchecked `byte_length + byte_offset > bufferLength` comparison. Both operands are caller-supplied `size_t` values, so on 64-bit builds their sum can **overflow and wrap** past the limit. When that happens: - the values are truncated to 32-bit for `JsCreateDataView` (yielding a small, valid view), but - the **original 64-bit** `byte_offset`/`byte_length` are stored in `DataViewInfo` and later returned by `napi_get_dataview_info` alongside the small backing buffer …handing a calling native addon an out-of-bounds read/write primitive (CWE-190 → CWE-125/787, also CWE-681). ## Fix Validate `byte_offset` and `byte_length` against the buffer size **individually, without adding them**: `cpp if (byte_offset > bufferLength || byte_length > bufferLength - byte_offset) { /* range error */ } ` After this check both values are `<= bufferLength` (a 32-bit quantity), so the later 32-bit truncation and the values stored in `DataViewInfo` are guaranteed in range. - **JavaScriptCore** backend delegates to the JS `DataView` constructor → unaffected. - **V8** backend carries the same upstream pattern but is vendored verbatim from Node → left untouched. ## Test This path is **not reachable from JS** (`new DataView` uses the engine directly, not this N-API), so coverage is a native gtest. It crafts an offset/length whose low 32 bits are valid for a 16-byte buffer but whose 64-bit sum wraps, and asserts the view is rejected (and never reports the raw 64-bit extents). Guarded to the Chakra engine and 64-bit builds (`JSRUNTIMEHOST_NAPI_ENGINE_CHAKRA` + `_WIN64`). Verified the test **fails on the pre-fix code** and **passes after the fix**; full suite green (5 native tests + 151 JS tests) on Win32-x64 Chakra Debug. Co-authored-by: Branimir Karadzic <branimirkaradzic@gmail.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 823e455 commit 99457c0

4 files changed

Lines changed: 93 additions & 2 deletions

File tree

Core/Node-API/Source/js_native_api_chakra.cc

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2244,7 +2244,13 @@ napi_status napi_create_dataview(napi_env env,
22442244
&unused,
22452245
&bufferLength));
22462246

2247-
if (byte_length + byte_offset > bufferLength) {
2247+
// bufferLength is 32-bit; byte_offset and byte_length are caller-supplied
2248+
// size_t values. Validate each against the buffer size without adding them
2249+
// (byte_offset + byte_length could overflow size_t and wrap past the check,
2250+
// after which the values would be truncated to 32-bit for JsCreateDataView
2251+
// while the original 64-bit values get stored in DataViewInfo and later
2252+
// handed back by napi_get_dataview_info, enabling an out-of-bounds access).
2253+
if (byte_offset > bufferLength || byte_length > bufferLength - byte_offset) {
22482254
napi_throw_range_error(
22492255
env,
22502256
"ERR_NAPI_INVALID_DATAVIEW_ARGS",

Core/Node-API/Source/js_native_api_v8.cc

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3185,7 +3185,13 @@ napi_status NAPI_CDECL napi_create_dataview(napi_env env,
31853185
RETURN_STATUS_IF_FALSE(env, value->IsArrayBuffer(), napi_invalid_arg);
31863186

31873187
v8::Local<v8::ArrayBuffer> buffer = value.As<v8::ArrayBuffer>();
3188-
if (byte_length + byte_offset > buffer->ByteLength()) {
3188+
// [BABYLON-NATIVE-ADDITION]: overflow-safe bounds check; not in Node.js upstream
3189+
// byte_offset and byte_length are caller-supplied size_t values; computing
3190+
// byte_length + byte_offset can overflow and wrap past the buffer size, which
3191+
// would slip an out-of-range request through and trip a fatal CHECK inside
3192+
// v8::DataView::New. Validate each against the buffer size without adding them.
3193+
if (byte_offset > buffer->ByteLength() ||
3194+
byte_length > buffer->ByteLength() - byte_offset) {
31893195
napi_throw_range_error(env,
31903196
"ERR_NAPI_INVALID_DATAVIEW_ARGS",
31913197
"byte_offset + byte_length should be less than or "

Tests/UnitTests/CMakeLists.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ endif()
4646
add_executable(UnitTests ${SOURCES} ${SCRIPTS} ${TYPE_SCRIPTS} ${ASSETS})
4747
target_compile_definitions(UnitTests PRIVATE JSRUNTIMEHOST_PLATFORM="${JSRUNTIMEHOST_PLATFORM}")
4848

49+
# The V8JSI Node-API shim does not implement napi_create_dataview, so the
50+
# CreateDataViewRejectsOverflowingRange test is compiled out on that backend.
51+
if(NAPI_JAVASCRIPT_ENGINE STREQUAL "JSI")
52+
target_compile_definitions(UnitTests PRIVATE JSRUNTIMEHOST_NAPI_ENGINE_JSI)
53+
endif()
54+
4955
target_link_libraries(UnitTests
5056
PRIVATE AppRuntime
5157
PRIVATE Console

Tests/UnitTests/Shared/Shared.cpp

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#include <arcana/threading/blocking_concurrent_queue.h>
1818
#include <atomic>
1919
#include <chrono>
20+
#include <cstdint>
2021
#include <future>
2122
#include <iostream>
2223
#include <thread>
@@ -278,6 +279,78 @@ TEST(AppRuntime, DestroyDoesNotDeadlock)
278279
testThread.join();
279280
}
280281

282+
// The V8JSI Node-API shim does not implement napi_create_dataview /
283+
// napi_get_dataview_info (its DataView::New throws "TODO"), so this native test
284+
// only builds on the Chakra, V8, and JavaScriptCore backends. The size_t-width
285+
// guard is required because the overflow scenario below needs a 64-bit size_t.
286+
#if (SIZE_MAX > 0xFFFFFFFFu) && !defined(JSRUNTIMEHOST_NAPI_ENGINE_JSI)
287+
TEST(NodeApi, CreateDataViewRejectsOverflowingRange)
288+
{
289+
// Regression: napi_create_dataview must reject a (byte_offset, byte_length)
290+
// pair whose sum overflows size_t. The pre-fix code performed an unchecked
291+
// `byte_offset + byte_length > bufferLength` comparison; with the inputs
292+
// below the 64-bit sum wraps to 8 and slips past it. It then truncated the
293+
// values to 32-bit (offset -> 0, length -> 8) and created a valid 8-byte
294+
// DataView, but stored the ORIGINAL 64-bit offset/length in DataViewInfo,
295+
// which napi_get_dataview_info hands back alongside the small real buffer --
296+
// an out-of-bounds access primitive. This path is not reachable from JS
297+
// `new DataView`, so it is covered natively here. The scenario requires a
298+
// 64-bit size_t (where the 32-bit truncation diverged from the stored value),
299+
// hence the size_t-width guard.
300+
Babylon::AppRuntime runtime{};
301+
302+
std::promise<bool> overflowSafe;
303+
std::promise<bool> validAccepted;
304+
305+
runtime.Dispatch([&overflowSafe, &validAccepted](Napi::Env env) {
306+
napi_env nenv{env};
307+
308+
Napi::ArrayBuffer arrayBuffer{Napi::ArrayBuffer::New(env, 16)};
309+
napi_value arrayBufferValue{arrayBuffer};
310+
311+
// Low 32 bits are individually valid for the 16-byte buffer (offset 0,
312+
// length 8), but the full 64-bit values are enormous and their sum wraps
313+
// around size_t to 8.
314+
const size_t hugeOffset{0xFFFFFFFF00000000ull};
315+
const size_t hugeLength{0x0000000100000008ull};
316+
317+
napi_value result{nullptr};
318+
napi_status status{napi_create_dataview(nenv, hugeLength, arrayBufferValue, hugeOffset, &result)};
319+
320+
bool safe;
321+
if (status != napi_ok || result == nullptr)
322+
{
323+
// Fixed path: the out-of-range request is rejected outright.
324+
safe = true;
325+
}
326+
else
327+
{
328+
// If creation unexpectedly succeeds, the reported extents must still
329+
// lie within the 16-byte backing buffer (i.e. not the raw 64-bit
330+
// inputs). The pre-fix code reported the huge stored values here.
331+
size_t reportedLength{0};
332+
size_t reportedOffset{0};
333+
void* data{nullptr};
334+
napi_get_dataview_info(nenv, result, &reportedLength, &data, nullptr, &reportedOffset);
335+
safe = reportedOffset <= 16 && reportedLength <= 16 && reportedOffset + reportedLength <= 16;
336+
}
337+
338+
// Clear any pending range error so it doesn't surface as an unhandled error.
339+
napi_value pendingException{nullptr};
340+
napi_get_and_clear_last_exception(nenv, &pendingException);
341+
overflowSafe.set_value(safe);
342+
343+
// A legitimate offset/length pair must still succeed.
344+
napi_value validResult{nullptr};
345+
napi_status validStatus{napi_create_dataview(nenv, 8, arrayBufferValue, 4, &validResult)};
346+
validAccepted.set_value(validStatus == napi_ok && validResult != nullptr);
347+
});
348+
349+
EXPECT_TRUE(overflowSafe.get_future().get());
350+
EXPECT_TRUE(validAccepted.get_future().get());
351+
}
352+
#endif
353+
281354
int RunTests()
282355
{
283356
testing::InitGoogleTest();

0 commit comments

Comments
 (0)