Skip to content

Commit a2520fa

Browse files
fix(worker): keep a forwarded worker error exact when its stack getter throws
Reading the stack of the error a worker's onerror threw could run a `stack` getter, and a getter that threw replaced that error in the TryCatch holding it, so the parent rebuilt the getter's error instead. The stack read now runs under its own TryCatch. The thrown value's name and message travel to the parent as UTF-16, so an unpaired surrogate arrives as thrown rather than as U+FFFD. ArgConverter::ConvertToString copies by length, so an embedded NUL no longer cuts a converted string short.
1 parent 7cca6e9 commit a2520fa

8 files changed

Lines changed: 84 additions & 29 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// The scope's onerror throws an error whose message holds a NUL and an
2+
// unpaired surrogate, and whose stack getter throws.
3+
onerror = function () {
4+
var error = new TypeError("before\0after \uD800");
5+
Object.defineProperty(error, "stack", {
6+
get: function () { throw new RangeError("thrown by the stack getter"); }
7+
});
8+
throw error;
9+
};
10+
onmessage = function () {
11+
throw new Error("thrown by onmessage");
12+
};

‎test-app/app/src/main/assets/app/tests/testMessaging.js‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,18 @@ describe("Messaging runtime edges", function () {
292292
});
293293
});
294294

295+
it("rebuilds the error a worker's onerror threw exactly, even when its stack getter throws", function (done) {
296+
var wt = require("node:worker_threads");
297+
var worker = new wt.Worker("~/tests/messaging/onerrorRethrowingWorker.js");
298+
worker.on("error", function (error) {
299+
expect(error instanceof TypeError).toBe(true);
300+
expect(error.message).toBe("before\0after \uD800");
301+
worker.terminate();
302+
done();
303+
});
304+
worker.postMessage("go");
305+
});
306+
295307
it("calls a node:worker_threads once listener once when an earlier listener emits again", function () {
296308
var wt = require("node:worker_threads");
297309
var worker = new wt.Worker("~/tests/eventLoopEchoWorker.js");

‎test-app/runtime/src/main/cpp/ArgConverter.h‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ class ArgConverter {
6060
} else {
6161
auto isolate = v8::Isolate::GetCurrent();
6262
v8::String::Utf8Value str(isolate, s);
63-
return {*str};
63+
return {*str, static_cast<size_t>(str.length())};
6464
}
6565
}
6666

‎test-app/runtime/src/main/cpp/CallbackHandlers.cpp‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1842,6 +1842,9 @@ static void ExtractTryCatchInfo(Isolate *isolate, Local<Context> context, TryCat
18421842
}
18431843
}
18441844

1845+
// `stack` may be an accessor. One that throws only costs the stack: caught
1846+
// here, its exception cannot replace the one `tc` holds.
1847+
TryCatch stackTc(isolate);
18451848
Local<Value> outStackTrace = tc.StackTrace(context).FromMaybe(Local<Value>());
18461849
if (!outStackTrace.IsEmpty()) {
18471850
Local<String> stackTraceStr =

‎test-app/runtime/src/main/cpp/WorkerEvents.cpp‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,8 @@ void WorkerEvents::EmitMessage(Isolate* isolate, Local<Object> receiver,
101101
MaybeLocal<Value> WorkerEvents::EmitError(Isolate* isolate, Local<Object> receiver,
102102
const std::string& message, const std::string& source,
103103
const std::string& stackTrace, int lineNumber,
104-
const std::string& errorName,
105-
const std::string& errorMessage) {
104+
Local<String> errorName,
105+
Local<String> errorMessage) {
106106
auto* state = RuntimeState::For<WorkerEventsState>(isolate);
107107
if (state == nullptr || state->emitError.IsEmpty()) {
108108
return MaybeLocal<Value>();
@@ -117,8 +117,8 @@ MaybeLocal<Value> WorkerEvents::EmitError(Isolate* isolate, Local<Object> receiv
117117
ArgConverter::ConvertToV8String(isolate, source),
118118
Number::New(isolate, lineNumber),
119119
ArgConverter::ConvertToV8String(isolate, stackTrace),
120-
ArgConverter::ConvertToV8String(isolate, errorName),
121-
ArgConverter::ConvertToV8String(isolate, errorMessage)};
120+
errorName,
121+
errorMessage};
122122
return state->emitError.Get(isolate)->Call(context, receiver, 6, args);
123123
}
124124

‎test-app/runtime/src/main/cpp/WorkerEvents.h‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ class WorkerEvents {
5353
const std::string& message,
5454
const std::string& source,
5555
const std::string& stackTrace, int lineNumber,
56-
const std::string& errorName,
57-
const std::string& errorMessage);
56+
v8::Local<v8::String> errorName,
57+
v8::Local<v8::String> errorMessage);
5858
};
5959

6060
} // namespace tns

‎test-app/runtime/src/main/cpp/WorkerWrapper.cpp‎

Lines changed: 48 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include <android/looper.h>
44
#include <pthread.h>
55

6+
#include <optional>
67
#include <thread>
78

89
#include "ArgConverter.h"
@@ -33,36 +34,57 @@ namespace tns {
3334
namespace {
3435

3536
/*
36-
* The `name` and `message` the parent rebuilds the worker's error from, read
37-
* off the value the worker threw. An object's `name` and `message` are taken
38-
* when they are strings, so an Error or a DOMException keeps both; anything
39-
* else becomes an Error whose message is the value's string form. Either
40-
* property may be a getter, and one that throws leaves the default in place.
37+
* The `name` and `message` the parent rebuilds the worker's error from. They
38+
* stay UTF-16 on the way, so both arrive exactly as thrown, embedded NULs and
39+
* unpaired surrogates included.
4140
*/
42-
void DescribeThrownValue(Isolate* isolate, Local<Context> context, Local<Value> thrown,
43-
std::string& name, std::string& message) {
41+
struct ThrownErrorText {
42+
std::u16string name = u"Error";
43+
std::u16string message;
44+
};
45+
46+
std::u16string ToUtf16(Isolate* isolate, Local<String> value) {
47+
std::u16string result(value->Length(), u'\0');
48+
value->WriteV2(isolate, 0, value->Length(), reinterpret_cast<uint16_t*>(result.data()));
49+
return result;
50+
}
51+
52+
Local<String> FromUtf16(Isolate* isolate, const std::u16string& value) {
53+
return String::NewFromTwoByte(isolate, reinterpret_cast<const uint16_t*>(value.data()),
54+
NewStringType::kNormal, static_cast<int>(value.size()))
55+
.ToLocalChecked();
56+
}
57+
58+
/*
59+
* Reads the text off the value the worker threw. An object's `name` and
60+
* `message` are taken when they are strings, so an Error or a DOMException
61+
* keeps both; anything else becomes an Error whose message is the value's
62+
* string form. Either property may be a getter, and one that throws leaves the
63+
* default in place.
64+
*/
65+
ThrownErrorText DescribeThrownValue(Isolate* isolate, Local<Context> context, Local<Value> thrown) {
4466
HandleScope handleScope(isolate);
4567
TryCatch tc(isolate);
46-
name = "Error";
47-
message.clear();
68+
ThrownErrorText text;
4869
Local<String> detail;
4970
if (thrown->ToDetailString(context).ToLocal(&detail)) {
50-
message = ArgConverter::ConvertToString(detail);
71+
text.message = ToUtf16(isolate, detail);
5172
}
5273
if (!thrown->IsObject() || thrown->IsFunction()) {
53-
return;
74+
return text;
5475
}
5576
auto object = thrown.As<Object>();
5677
Local<Value> value;
5778
if (object->Get(context, ArgConverter::ConvertToV8String(isolate, "name")).ToLocal(&value) &&
5879
value->IsString()) {
59-
name = ArgConverter::ConvertToString(value.As<String>());
80+
text.name = ToUtf16(isolate, value.As<String>());
6081
}
6182
if (object->Get(context, ArgConverter::ConvertToV8String(isolate, "message"))
6283
.ToLocal(&value) &&
6384
value->IsString()) {
64-
message = ArgConverter::ConvertToString(value.As<String>());
85+
text.message = ToUtf16(isolate, value.As<String>());
6586
}
87+
return text;
6688
}
6789

6890
/*
@@ -416,26 +438,32 @@ void WorkerWrapper::PassUncaughtExceptionFromWorkerToParent(const std::string& m
416438

417439
// Read here, on the worker's isolate. A report with no thrown value, such
418440
// as the heap-limit one made from inside a GC, touches no v8 handle.
419-
std::string errorName = "Error";
420-
std::string errorMessage = message;
441+
std::optional<ThrownErrorText> thrownText;
421442
if (!thrown.IsEmpty()) {
422443
Isolate* workerIsolate = Isolate::GetCurrent();
423-
DescribeThrownValue(workerIsolate, workerIsolate->GetCurrentContext(), thrown, errorName,
424-
errorMessage);
444+
thrownText = DescribeThrownValue(workerIsolate, workerIsolate->GetCurrentContext(), thrown);
425445
}
426446

427447
int workerId = workerId_;
428448
std::string threadName = threadName_;
429449
Isolate* parentIsolate = parentIsolate_;
430450

431451
parentTasks->PostInternal([workerId, message, filename, stackTrace, lineno, threadName,
432-
parentIsolate, errorName, errorMessage]() {
452+
parentIsolate, thrownText]() {
433453
v8::Locker locker(parentIsolate);
434454
Isolate::Scope isolate_scope(parentIsolate);
435455
HandleScope handle_scope(parentIsolate);
436456
auto context = Runtime::GetRuntime(parentIsolate)->GetContext();
437457
Context::Scope context_scope(context);
438458

459+
// Without a thrown value, the error is an Error carrying the report's
460+
// message.
461+
Local<String> errorName = thrownText
462+
? FromUtf16(parentIsolate, thrownText->name)
463+
: ArgConverter::ConvertToV8String(parentIsolate, "Error");
464+
Local<String> errorMessage = thrownText
465+
? FromUtf16(parentIsolate, thrownText->message)
466+
: ArgConverter::ConvertToV8String(parentIsolate, message);
439467
WorkerWrapper::FireErrorOnParentWorkerObject(workerId, message, stackTrace, filename,
440468
lineno, threadName, errorName,
441469
errorMessage);
@@ -446,8 +474,8 @@ void WorkerWrapper::FireErrorOnParentWorkerObject(int workerId, const std::strin
446474
const std::string& stackTrace,
447475
const std::string& filename, int lineno,
448476
const std::string& threadName,
449-
const std::string& errorName,
450-
const std::string& errorMessage) {
477+
Local<String> errorName,
478+
Local<String> errorMessage) {
451479
auto wrapper = WorkerWrapper::GetById(workerId);
452480
if (wrapper == nullptr) {
453481
DEBUG_WRITE("MAIN: no worker instance was found with workerId=%d.", workerId);

‎test-app/runtime/src/main/cpp/WorkerWrapper.h‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,8 @@ class WorkerWrapper : public std::enable_shared_from_this<WorkerWrapper> {
176176
const std::string& stackTrace,
177177
const std::string& filename, int lineno,
178178
const std::string& threadName,
179-
const std::string& errorName,
180-
const std::string& errorMessage);
179+
v8::Local<v8::String> errorName,
180+
v8::Local<v8::String> errorMessage);
181181

182182
v8::Isolate* parentIsolate_;
183183
// The parent runtime's task queue; weak so a child outliving its parent

0 commit comments

Comments
 (0)