Skip to content

Commit 65ee614

Browse files
committed
fix: unique JS error handle ids, and diagnose bad metadata node ids
Two review findings on this branch. The JS error handle id was minted from a per-runtime counter, so every runtime produced 1, 2, 3... A throwable converted back to JS on a runtime other than the one that created it would then find an unrelated entry under the same id and consume it, instead of missing and falling back to rebuilding the error from the Java throwable. Ids are now unique process-wide, which is what makes the table lookup itself the ownership check. GetNodeById's new bounds check turned an out-of-range read into a nullptr its callers still dereferenced. It now logs the offending id, ReadTypeName and the array-element lookup in GetNodeType throw a NativeScriptException naming the problem, and GetBaseClassNode returns null -- which every caller already treats as "no base class". The assert it relied on was a no-op in release, where the bounds check was missing entirely. Also asserts the ownership precondition in StateMutex::Unlock and ReleaseAll: an unmatched unlock would wrap depth_ and hold the mutex forever, and a non-owner ReleaseAll would drop another thread's lock mid-section.
1 parent 96e1ff2 commit 65ee614

2 files changed

Lines changed: 36 additions & 8 deletions

File tree

test-app/runtime/src/main/cpp/MetadataReader.cpp

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include "MetadataReader.h"
22
#include "MetadataMethodInfo.h"
33
#include <android/log.h>
4+
#include "NativeScriptException.h"
45
#include "Util.h"
56
#include <sstream>
67

@@ -40,6 +41,8 @@ void MetadataReader::StateMutex::Lock() {
4041

4142
void MetadataReader::StateMutex::Unlock() {
4243
std::lock_guard<std::mutex> guard(mutex_);
44+
// An unmatched unlock would wrap depth_ and hold the mutex forever.
45+
assert(depth_ > 0 && owner_ == std::this_thread::get_id());
4346
if (--depth_ == 0) {
4447
owner_ = std::thread::id();
4548
// Every waiter is blocked on the same `depth_ == 0`, so waking one is
@@ -50,6 +53,9 @@ void MetadataReader::StateMutex::Unlock() {
5053

5154
unsigned MetadataReader::StateMutex::ReleaseAll() {
5255
std::lock_guard<std::mutex> guard(mutex_);
56+
// Only the owner may release: doing this from a non-owner would drop
57+
// another thread's lock while it is still inside its guarded section.
58+
assert(depth_ > 0 && owner_ == std::this_thread::get_id());
5359
unsigned held = depth_;
5460
depth_ = 0;
5561
owner_ = std::thread::id();
@@ -144,6 +150,9 @@ MetadataTreeNode *MetadataReader::GetNodeById(uint16_t nodeId) {
144150
StateLock lock(m_stateMutex);
145151

146152
if (nodeId >= m_v.size()) {
153+
__android_log_print(ANDROID_LOG_ERROR, "TNS.error",
154+
"Metadata node id %u is out of range (%zu nodes). The metadata is inconsistent.",
155+
nodeId, m_v.size());
147156
return nullptr;
148157
}
149158

@@ -152,6 +161,12 @@ MetadataTreeNode *MetadataReader::GetNodeById(uint16_t nodeId) {
152161

153162

154163
string MetadataReader::ReadTypeName(MetadataTreeNode *treeNode) {
164+
// Guards the whole family: the uint16_t overload and ReadTypeNameInternal's
165+
// array-element forward both reach here with a GetNodeById result.
166+
if (treeNode == nullptr) {
167+
throw NativeScriptException("Cannot read a type name: the metadata refers to a node that does not exist.");
168+
}
169+
155170
StateLock lock(m_stateMutex);
156171

157172
string name;
@@ -245,6 +260,9 @@ uint8_t MetadataReader::GetNodeType(MetadataTreeNode *treeNode) {
245260
} else {
246261
uint16_t nodeId = offsetValue - ARRAY_OFFSET;
247262
MetadataTreeNode * arrElemNode = GetNodeById(nodeId);
263+
if (arrElemNode == nullptr) {
264+
throw NativeScriptException("Cannot resolve an array element type: the metadata refers to a node that does not exist.");
265+
}
248266
nodeType = *(m_valueData + arrElemNode->offsetValue);
249267
}
250268

@@ -425,10 +443,8 @@ MetadataTreeNode *MetadataReader::GetBaseClassNode(MetadataTreeNode *treeNode) {
425443
uint16_t baseClassNodeId = *reinterpret_cast<uint16_t *>(m_valueData +
426444
treeNode->offsetValue + 1);
427445

428-
size_t nodeCount = m_v.size();
429-
430-
assert(baseClassNodeId < nodeCount);
431-
446+
// Null for an id the metadata got wrong, which GetNodeById logs; every
447+
// caller already treats a missing base class as "no base class".
432448
baseClassNode = GetNodeById(baseClassNodeId);
433449
}
434450

test-app/runtime/src/main/cpp/NativeScriptException.cpp

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include "NativeScriptException.h"
22

33
#include <algorithm>
4+
#include <atomic>
45
#include <sstream>
56

67
#include "ArgConverter.h"
@@ -76,14 +77,25 @@ struct JsErrorHandles {
7677
pruneAt = std::max<size_t>(16, entries.size() * 2);
7778
}
7879

79-
int64_t nextId = 1;
8080
size_t pruneAt = 16;
8181
robin_hood::unordered_map<int64_t, Entry> entries;
8282
};
8383

8484
/*
85-
* Files `error` under a fresh id for this runtime. Returns 0 when the runtime
86-
* is tearing down, which the Java side reads as "no JS value".
85+
* Ids are unique process-wide rather than per runtime. A throwable can be
86+
* converted back to JS on a runtime other than the one that created it (it
87+
* outlives its runtime, or crosses threads), and a per-runtime counter would
88+
* make that lookup hit an unrelated entry with the same id instead of missing.
89+
* Being globally unique is what makes `entries.find(id)` the ownership check:
90+
* a foreign id is simply absent, and the caller falls back to rebuilding the
91+
* error from the Java throwable.
92+
*/
93+
std::atomic<int64_t> g_nextJsErrorId{1};
94+
95+
/*
96+
* Files `error` in this runtime's table under a process-wide unique id.
97+
* Returns 0 when the runtime is tearing down, which the Java side reads as
98+
* "no JS value".
8799
*/
88100
int64_t StoreJsError(Isolate* isolate, Local<Value> error) {
89101
auto* handles = RuntimeState::For<JsErrorHandles>(isolate);
@@ -94,7 +106,7 @@ int64_t StoreJsError(Isolate* isolate, Local<Value> error) {
94106
JEnv env;
95107
handles->PruneIfDue(env);
96108

97-
int64_t id = handles->nextId++;
109+
int64_t id = g_nextJsErrorId.fetch_add(1, std::memory_order_relaxed);
98110
JsErrorHandles::Entry entry;
99111
entry.value.Reset(isolate, error);
100112
handles->entries.emplace(id, std::move(entry));

0 commit comments

Comments
 (0)