Skip to content

Commit a553dd1

Browse files
bkaradzicCopilot
authored andcommitted
Fix integer overflow in napi_create_dataview bounds check
napi_create_dataview validated the requested view with an unchecked byte_length + byte_offset > bufferLength comparison. Both are caller-supplied size_t values, so on 64-bit builds their sum can overflow and wrap past the limit, slipping an out-of-range view past the check. - Chakra: the truncated 32-bit values created a small valid view while the original 64-bit extents were stored in DataViewInfo and handed back by napi_get_dataview_info, giving an out-of-bounds access primitive. - V8: the wrapped value passed the check and then tripped a fatal CHECK inside v8::DataView::New, aborting the process. Both backends now validate byte_offset and byte_length against the buffer size individually, without adding them. JavaScriptCore delegates to the JS DataView constructor and is unaffected. The V8JSI Node-API shim does not implement napi_create_dataview (its DataView::New throws "TODO"), so the regression test is compiled out there; it runs on Chakra, V8, and JavaScriptCore on 64-bit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 000f5c9 commit a553dd1

4 files changed

Lines changed: 92 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: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3185,7 +3185,12 @@ 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+
// byte_offset and byte_length are caller-supplied size_t values; computing
3189+
// byte_length + byte_offset can overflow and wrap past the buffer size, which
3190+
// would slip an out-of-range request through and trip a fatal CHECK inside
3191+
// v8::DataView::New. Validate each against the buffer size without adding them.
3192+
if (byte_offset > buffer->ByteLength() ||
3193+
byte_length > buffer->ByteLength() - byte_offset) {
31893194
napi_throw_range_error(env,
31903195
"ERR_NAPI_INVALID_DATAVIEW_ARGS",
31913196
"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
@@ -16,6 +16,7 @@
1616
#include <arcana/threading/blocking_concurrent_queue.h>
1717
#include <atomic>
1818
#include <chrono>
19+
#include <cstdint>
1920
#include <future>
2021
#include <iostream>
2122
#include <thread>
@@ -276,6 +277,78 @@ TEST(AppRuntime, DestroyDoesNotDeadlock)
276277
testThread.join();
277278
}
278279

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

0 commit comments

Comments
 (0)