-
Notifications
You must be signed in to change notification settings - Fork 23
Expand file tree
/
Copy pathShared.cpp
More file actions
410 lines (351 loc) · 16.1 KB
/
Copy pathShared.cpp
File metadata and controls
410 lines (351 loc) · 16.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
#include "Shared.h"
#include <Babylon/AppRuntime.h>
#include <Babylon/ScriptLoader.h>
#include <Babylon/Polyfills/AbortController.h>
#include <Babylon/Polyfills/Console.h>
#include <Babylon/Polyfills/Performance.h>
#include <Babylon/Polyfills/Scheduling.h>
#include <Babylon/Polyfills/URL.h>
#include <Babylon/Polyfills/WebSocket.h>
#include <Babylon/Polyfills/XMLHttpRequest.h>
#include <Babylon/Polyfills/Fetch.h>
#include <Babylon/Polyfills/Blob.h>
#include <Babylon/Polyfills/File.h>
#include <Babylon/Polyfills/TextDecoder.h>
#include <Babylon/Polyfills/TextEncoder.h>
#include <gtest/gtest.h>
#include <arcana/threading/blocking_concurrent_queue.h>
#include <atomic>
#include <chrono>
#include <cstdint>
#include <future>
#include <iostream>
#include <thread>
namespace
{
const char* EnumToString(Babylon::Polyfills::Console::LogLevel logLevel)
{
switch (logLevel)
{
case Babylon::Polyfills::Console::LogLevel::Log:
return "log";
case Babylon::Polyfills::Console::LogLevel::Warn:
return "warn";
case Babylon::Polyfills::Console::LogLevel::Error:
return "error";
}
return "unknown";
}
}
TEST(JavaScript, All)
{
// Change this to true to wait for the JavaScript debugger to attach (only applies to V8)
constexpr const bool waitForDebugger = false;
std::promise<int32_t> exitCodePromise;
Babylon::AppRuntime::Options options{};
options.UnhandledExceptionHandler = [&exitCodePromise](const Napi::Error& error) {
std::cerr << "[Uncaught Error] " << Napi::GetErrorString(error) << std::endl;
std::cerr.flush();
exitCodePromise.set_value(-1);
};
if (waitForDebugger)
{
std::cout << "Waiting for debugger..." << std::endl;
options.WaitForDebugger = true;
}
Babylon::AppRuntime runtime{options};
runtime.Dispatch([&exitCodePromise](Napi::Env env) mutable {
Babylon::Polyfills::Console::Initialize(env, [env](const char* message, Babylon::Polyfills::Console::LogLevel logLevel) {
std::cout << "[" << EnumToString(logLevel) << "] " << message;
if (logLevel == Babylon::Polyfills::Console::LogLevel::Error)
{
std::string stack = Babylon::Polyfills::Console::CaptureCurrentJsStack(env);
if (!stack.empty())
{
std::cout << std::endl << stack;
}
}
std::cout << std::endl;
std::cout.flush();
});
Babylon::Polyfills::AbortController::Initialize(env);
Babylon::Polyfills::Performance::Initialize(env);
Babylon::Polyfills::Scheduling::Initialize(env);
Babylon::Polyfills::URL::Initialize(env);
Babylon::Polyfills::WebSocket::Initialize(env);
Babylon::Polyfills::XMLHttpRequest::Initialize(env);
Babylon::Polyfills::Fetch::Initialize(env);
Babylon::Polyfills::Blob::Initialize(env);
Babylon::Polyfills::File::Initialize(env);
Babylon::Polyfills::TextDecoder::Initialize(env);
Babylon::Polyfills::TextEncoder::Initialize(env);
auto setExitCodeCallback = Napi::Function::New(
env, [&exitCodePromise](const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
exitCodePromise.set_value(info[0].As<Napi::Number>().Int32Value());
},
"setExitCode");
env.Global().Set("setExitCode", setExitCodeCallback);
env.Global().Set("hostPlatform", Napi::Value::From(env, JSRUNTIMEHOST_PLATFORM));
});
Babylon::ScriptLoader loader{runtime};
loader.Eval("location = { href: '' };", ""); // Required for Mocha.js as we do not have a location
loader.LoadScript("app:///Scripts/tests.js");
auto exitCode{exitCodePromise.get_future().get()};
EXPECT_EQ(exitCode, 0);
}
TEST(Console, Log)
{
Babylon::AppRuntime runtime{};
runtime.Dispatch([](Napi::Env env) mutable {
Babylon::Polyfills::Console::Initialize(env, [](const char* message, Babylon::Polyfills::Console::LogLevel logLevel) {
const char* test = "foo bar";
if (strcmp(message, test) != 0)
{
std::cout << "Expected: " << test << std::endl;
std::cout << "Received: " << message << std::endl;
std::cout.flush();
ADD_FAILURE();
}
});
});
std::promise<void> done;
Babylon::ScriptLoader loader{runtime};
loader.Eval("console.log('foo', 'bar')", "");
loader.Dispatch([&done](auto) {
done.set_value();
});
done.get_future().get();
}
TEST(Console, CaptureCurrentJsStack)
{
// Regression: Console::CaptureCurrentJsStack must return a non-empty stack when called from
// within a callback fired by `console.error`, and when called from `console.log` (any frame
// produced by JS execution).
Babylon::AppRuntime runtime{};
std::promise<std::string> errorStackPromise;
std::promise<std::string> logStackPromise;
runtime.Dispatch([&errorStackPromise, &logStackPromise](Napi::Env env) mutable {
Babylon::Polyfills::Console::Initialize(env, [env, &errorStackPromise, &logStackPromise](const char* /*message*/, Babylon::Polyfills::Console::LogLevel logLevel) {
std::string stack = Babylon::Polyfills::Console::CaptureCurrentJsStack(env);
if (logLevel == Babylon::Polyfills::Console::LogLevel::Error)
{
errorStackPromise.set_value(std::move(stack));
}
else if (logLevel == Babylon::Polyfills::Console::LogLevel::Log)
{
logStackPromise.set_value(std::move(stack));
}
});
});
Babylon::ScriptLoader loader{runtime};
loader.Eval("console.log('log message');", "");
loader.Eval("function inner() { console.error('error message'); } inner();", "");
auto errorFuture = errorStackPromise.get_future();
auto logFuture = logStackPromise.get_future();
constexpr auto timeout = std::chrono::seconds(30);
ASSERT_EQ(errorFuture.wait_for(timeout), std::future_status::ready)
<< "console.error callback did not fire within timeout";
ASSERT_EQ(logFuture.wait_for(timeout), std::future_status::ready)
<< "console.log callback did not fire within timeout";
std::string errorStack = errorFuture.get();
std::string logStack = logFuture.get();
EXPECT_FALSE(errorStack.empty()) << "console.error path must capture a non-empty JS stack";
EXPECT_FALSE(logStack.empty()) << "console.log path must capture a non-empty JS stack";
}
TEST(AppRuntime, DestroyDoesNotDeadlock)
{
// Regression test verifying AppRuntime destruction doesn't deadlock.
// Uses a global arcana hook to sleep while holding the queue mutex
// before wait(), ensuring the worker is in the vulnerable window
// when the destructor fires. See #147 for details on the bug and fix.
//
// The entire test runs on a separate thread so the gtest thread can
// detect a deadlock via timeout without hanging the process.
//
// Test flow:
//
// Test Thread Worker Thread
// ----------- -------------
// 1. Create AppRuntime Worker starts, enters blocking_tick
// Wait for init to complete
// 2. Install hook
// Dispatch(no-op) Worker wakes, runs no-op,
// returns to blocking_tick
// Hook fires:
// signal workerInHook
// sleep 200ms (holding mutex!)
// 3. workerInHook.wait()
// Worker is sleeping in hook
// 4. ~AppRuntime():
// cancel()
// Append(no-op):
// push() blocks ------> (worker holds mutex)
// 200ms sleep ends
// wait(lock) releases mutex
// push() acquires mutex
// pushes, notifies ---> wakes up!
// join() waits drains no-op, cancelled -> exit
// join() returns <----- thread exits
// 5. destroy completes -> PASS
bool hookSignaled{false};
std::promise<void> workerInHook;
std::promise<void> testDone;
// Run the full lifecycle on a separate thread so the gtest thread
// can detect a deadlock via timeout.
std::thread testThread([&]() {
auto runtime = std::make_unique<Babylon::AppRuntime>();
// Wait for the runtime to fully initialize. The constructor dispatches
// CreateForJavaScript which must complete before we install the hook
// so the worker is idle and ready to enter the hook on the next wait.
std::promise<void> ready;
runtime->Dispatch([&ready](Napi::Env) {
ready.set_value();
});
ready.get_future().wait();
// Install the hook and dispatch a no-op to wake the worker,
// ensuring it cycles through the hook on its way back to idle.
arcana::test_hooks::blocking_concurrent_queue::set_before_wait_callback([&]() {
if (hookSignaled)
{
return;
}
hookSignaled = true;
workerInHook.set_value();
// This sleep is not truly deterministic. Its purpose is to hold the
// mutex long enough for runtime.reset() (called by the test thread
// after workerInHook signals) to reach push() while the mutex is
// still held. When the sleep ends, the worker enters wait() which
// releases the mutex, allowing push() to acquire it and deliver the
// wake-up notification. If runtime.reset() hasn't reached push()
// by the time the sleep ends, the test still passes but doesn't
// exercise the intended contention window.
std::this_thread::sleep_for(std::chrono::milliseconds(200));
});
runtime->Dispatch([](Napi::Env) {});
// Wait for the worker to be in the hook (holding mutex, sleeping)
workerInHook.get_future().wait();
// Destroy — if the fix works, the destructor completes.
// If broken, it deadlocks and the timeout detects it.
runtime.reset();
testDone.set_value();
});
auto status = testDone.get_future().wait_for(std::chrono::seconds(5));
arcana::test_hooks::blocking_concurrent_queue::set_before_wait_callback([]() {});
if (status == std::future_status::timeout)
{
testThread.detach();
FAIL() << "Deadlock detected: AppRuntime destructor did not complete within 5 seconds";
}
testThread.join();
}
// The V8JSI Node-API shim does not implement napi_create_dataview /
// napi_get_dataview_info (its DataView::New throws "TODO"), so this native test
// only builds on the Chakra, V8, and JavaScriptCore backends. The size_t-width
// guard is required because the overflow scenario below needs a 64-bit size_t.
#if (SIZE_MAX > 0xFFFFFFFFu) && !defined(JSRUNTIMEHOST_NAPI_ENGINE_JSI)
TEST(NodeApi, CreateDataViewRejectsOverflowingRange)
{
// Regression: napi_create_dataview must reject a (byte_offset, byte_length)
// pair whose sum overflows size_t. The pre-fix code performed an unchecked
// `byte_offset + byte_length > bufferLength` comparison; with the inputs
// below the 64-bit sum wraps to 8 and slips past it. It then truncated the
// values to 32-bit (offset -> 0, length -> 8) and created a valid 8-byte
// DataView, but stored the ORIGINAL 64-bit offset/length in DataViewInfo,
// which napi_get_dataview_info hands back alongside the small real buffer --
// an out-of-bounds access primitive. This path is not reachable from JS
// `new DataView`, so it is covered natively here. The scenario requires a
// 64-bit size_t (where the 32-bit truncation diverged from the stored value),
// hence the size_t-width guard.
Babylon::AppRuntime runtime{};
std::promise<bool> overflowSafe;
std::promise<bool> validAccepted;
runtime.Dispatch([&overflowSafe, &validAccepted](Napi::Env env) {
napi_env nenv{env};
Napi::ArrayBuffer arrayBuffer{Napi::ArrayBuffer::New(env, 16)};
napi_value arrayBufferValue{arrayBuffer};
// Low 32 bits are individually valid for the 16-byte buffer (offset 0,
// length 8), but the full 64-bit values are enormous and their sum wraps
// around size_t to 8.
const size_t hugeOffset{0xFFFFFFFF00000000ull};
const size_t hugeLength{0x0000000100000008ull};
napi_value result{nullptr};
napi_status status{napi_create_dataview(nenv, hugeLength, arrayBufferValue, hugeOffset, &result)};
bool safe;
if (status != napi_ok || result == nullptr)
{
// Fixed path: the out-of-range request is rejected outright.
safe = true;
}
else
{
// If creation unexpectedly succeeds, the reported extents must still
// lie within the 16-byte backing buffer (i.e. not the raw 64-bit
// inputs). The pre-fix code reported the huge stored values here.
size_t reportedLength{0};
size_t reportedOffset{0};
void* data{nullptr};
napi_get_dataview_info(nenv, result, &reportedLength, &data, nullptr, &reportedOffset);
safe = reportedOffset <= 16 && reportedLength <= 16 && reportedOffset + reportedLength <= 16;
}
// Clear any pending range error so it doesn't surface as an unhandled error.
napi_value pendingException{nullptr};
napi_get_and_clear_last_exception(nenv, &pendingException);
overflowSafe.set_value(safe);
// A legitimate offset/length pair must still succeed.
napi_value validResult{nullptr};
napi_status validStatus{napi_create_dataview(nenv, 8, arrayBufferValue, 4, &validResult)};
validAccepted.set_value(validStatus == napi_ok && validResult != nullptr);
});
EXPECT_TRUE(overflowSafe.get_future().get());
EXPECT_TRUE(validAccepted.get_future().get());
}
#endif
// The V8JSI Node-API shim does not expose napi_get_value_string_utf16, so this
// native test only builds on the Chakra, V8, and JavaScriptCore backends.
#if !defined(JSRUNTIMEHOST_NAPI_ENGINE_JSI)
TEST(NodeApi, GetValueStringUtf16HandlesZeroBufsize)
{
// Regression: napi_get_value_string_utf16 with a non-null buffer and
// bufsize == 0 must not evaluate bufsize - 1. On the Chakra backend the
// pre-fix code forwarded bufsize - 1 (== SIZE_MAX) to JsCopyStringUtf16 as
// the destination capacity, copying the entire JS string into the
// zero-length buffer, and then stored the terminator at buf[bufsize - 1]
// (== buf[SIZE_MAX]). The call must instead write nothing and report zero.
Babylon::AppRuntime runtime{};
std::promise<bool> zeroSafe;
std::promise<bool> normalWorks;
runtime.Dispatch([&zeroSafe, &normalWorks](Napi::Env env) {
napi_env nenv{env};
napi_value strValue{Napi::String::New(env, "hello world")};
// Sentinel-filled buffer. With bufsize == 0 nothing may be written, so
// every element must survive unchanged (a SIZE_MAX-capacity copy would
// clobber it / overflow).
char16_t guard[8];
for (auto& c : guard)
{
c = static_cast<char16_t>(0x7FFF);
}
size_t copied{0xDEAD};
napi_status status{napi_get_value_string_utf16(nenv, strValue, guard, 0, &copied)};
bool safe{status == napi_ok && copied == 0};
for (auto c : guard)
{
safe = safe && (c == static_cast<char16_t>(0x7FFF));
}
zeroSafe.set_value(safe);
// A sufficiently-sized buffer must still copy and null-terminate.
char16_t buf[32];
size_t copied2{0};
napi_status status2{napi_get_value_string_utf16(nenv, strValue, buf, 32, &copied2)};
normalWorks.set_value(status2 == napi_ok && copied2 == 11 && buf[copied2] == 0);
});
EXPECT_TRUE(zeroSafe.get_future().get());
EXPECT_TRUE(normalWorks.get_future().get());
}
#endif
int RunTests()
{
testing::InitGoogleTest();
return RUN_ALL_TESTS();
}