Skip to content

Commit 7c4bd47

Browse files
bkaradzicCopilot
authored andcommitted
Fix integer overflow in Chakra napi_create_dataview bounds check
napi_create_dataview validated the requested view with an unchecked �yte_length + byte_offset > bufferLength comparison. byte_length and byte_offset 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 then truncated to 32-bit for JsCreateDataView (creating a small, valid view) while the ORIGINAL 64-bit values are stored in DataViewInfo and later returned by napi_get_dataview_info alongside the small backing buffer, giving a calling addon an out-of-bounds read/write primitive. Validate byte_offset and byte_length against the buffer size individually without adding them, so neither overflow nor the subsequent 32-bit truncation can slip an out-of-range view past the check. After the check both values are <= bufferLength (a 32-bit quantity), so the truncation and the stored values are guaranteed in range. The JavaScriptCore backend delegates to the JS DataView constructor and is unaffected. The V8 backend carries the same upstream pattern but is vendored verbatim from Node and left untouched. Add a native regression test (the path is not reachable from JS ew DataView) that crafts an offset/length whose low 32 bits are valid but whose 64-bit sum wraps, and asserts the view is rejected (or at least never reports the raw 64-bit extents). Guarded to the Chakra engine and 64-bit builds. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 000f5c9 commit 7c4bd47

2 files changed

Lines changed: 76 additions & 1 deletion

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",

Tests/UnitTests/Shared/Shared.cpp

Lines changed: 69 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,74 @@ TEST(AppRuntime, DestroyDoesNotDeadlock)
276277
testThread.join();
277278
}
278279

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

0 commit comments

Comments
 (0)