@qntx/wallet-native 0.0.0-stage → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +123 -2
- package/WalletNative.podspec +74 -0
- package/android/CMakeLists.txt +99 -0
- package/android/build.gradle +177 -0
- package/android/consumer-rules.pro +4 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/cpp-adapter.cpp +29 -0
- package/android/src/main/java/com/margelo/nitro/qntxwallet/AppLifecycle.kt +35 -0
- package/android/src/main/java/com/margelo/nitro/qntxwallet/KernelPbkdf2.java +59 -0
- package/android/src/main/java/com/margelo/nitro/qntxwallet/Keystore.kt +280 -0
- package/android/src/main/java/com/margelo/nitro/qntxwallet/Passkey.kt +147 -0
- package/android/src/main/java/com/margelo/nitro/qntxwallet/VaultDirectory.kt +28 -0
- package/android/src/main/java/com/margelo/nitro/qntxwallet/WalletNativePackage.java +30 -0
- package/cpp/host/HybridKernelHost.cpp +162 -0
- package/cpp/host/HybridKernelHost.hpp +49 -0
- package/cpp/host/KernelFile.cpp +120 -0
- package/cpp/host/KernelFile.hpp +24 -0
- package/cpp/host/Keystore.hpp +70 -0
- package/cpp/host/Lifecycle.cpp +77 -0
- package/cpp/host/Lifecycle.hpp +71 -0
- package/cpp/host/Passkey.hpp +39 -0
- package/cpp/host/VaultDirectory.hpp +16 -0
- package/cpp/platform/KeystoreAndroid.cpp +175 -0
- package/cpp/platform/KeystoreIos.mm +300 -0
- package/cpp/platform/LifecycleAndroid.cpp +55 -0
- package/cpp/platform/LifecycleIos.mm +36 -0
- package/cpp/platform/PasskeyAndroid.cpp +118 -0
- package/cpp/platform/PasskeyIos.mm +20 -0
- package/cpp/platform/VaultDirectory.mm +58 -0
- package/cpp/platform/VaultDirectoryAndroid.cpp +35 -0
- package/cpp/runtime/KernelBackends.cpp +297 -0
- package/cpp/runtime/KernelBackends.hpp +20 -0
- package/cpp/runtime/KernelBuffer.cpp +12 -0
- package/cpp/runtime/KernelBuffer.hpp +96 -0
- package/cpp/runtime/KernelGlobals.cpp +535 -0
- package/cpp/runtime/KernelGlobals.hpp +107 -0
- package/cpp/runtime/KernelJs.cpp +155 -0
- package/cpp/runtime/KernelJs.hpp +81 -0
- package/cpp/runtime/KernelKeystore.cpp +224 -0
- package/cpp/runtime/KernelKeystore.hpp +28 -0
- package/cpp/runtime/KernelPasskey.cpp +137 -0
- package/cpp/runtime/KernelPasskey.hpp +23 -0
- package/cpp/runtime/KernelRuntime.cpp +496 -0
- package/cpp/runtime/KernelRuntime.hpp +94 -0
- package/cpp/runtime/platform/KernelPbkdf2.cpp +198 -0
- package/cpp/runtime/platform/KernelPbkdf2.hpp +38 -0
- package/dist/index.d.mts +44 -0
- package/dist/index.mjs +170 -0
- package/dist/index.mjs.map +1 -0
- package/kernel/generated/kernel-entry.js +42967 -0
- package/kernel/generated/kernel.manifest.json +80 -0
- package/nitrogen/generated/.gitattributes +1 -0
- package/nitrogen/generated/android/WalletNative+autolinking.cmake +81 -0
- package/nitrogen/generated/android/WalletNative+autolinking.gradle +27 -0
- package/nitrogen/generated/android/WalletNativeOnLoad.cpp +49 -0
- package/nitrogen/generated/android/WalletNativeOnLoad.hpp +34 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/qntxwallet/WalletNativeOnLoad.kt +35 -0
- package/nitrogen/generated/ios/WalletNative+autolinking.rb +62 -0
- package/nitrogen/generated/ios/WalletNative-Swift-Cxx-Bridge.cpp +17 -0
- package/nitrogen/generated/ios/WalletNative-Swift-Cxx-Bridge.hpp +27 -0
- package/nitrogen/generated/ios/WalletNative-Swift-Cxx-Umbrella.hpp +38 -0
- package/nitrogen/generated/ios/WalletNativeAutolinking.mm +35 -0
- package/nitrogen/generated/ios/WalletNativeAutolinking.swift +16 -0
- package/nitrogen/generated/shared/c++/HybridKernelHostSpec.cpp +25 -0
- package/nitrogen/generated/shared/c++/HybridKernelHostSpec.hpp +69 -0
- package/nitrogen/generated/shared/c++/KernelExitReason.hpp +84 -0
- package/package.json +61 -4
- package/react-native.config.js +14 -0
- package/scripts/compile-kernel.mjs +118 -0
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
#include "KernelRuntime.hpp"
|
|
2
|
+
|
|
3
|
+
#include "KernelBuffer.hpp"
|
|
4
|
+
#include "KernelGlobals.hpp"
|
|
5
|
+
|
|
6
|
+
#include <jsi/hermes-interfaces.h>
|
|
7
|
+
|
|
8
|
+
#if defined(__ANDROID__)
|
|
9
|
+
#include "platform/KernelPbkdf2.hpp"
|
|
10
|
+
#include <fbjni/fbjni.h>
|
|
11
|
+
#include <jni.h>
|
|
12
|
+
#endif
|
|
13
|
+
|
|
14
|
+
#include <atomic>
|
|
15
|
+
#include <condition_variable>
|
|
16
|
+
#include <deque>
|
|
17
|
+
#include <map>
|
|
18
|
+
#include <mutex>
|
|
19
|
+
#include <optional>
|
|
20
|
+
#include <stdexcept>
|
|
21
|
+
#include <thread>
|
|
22
|
+
#include <unordered_map>
|
|
23
|
+
#include <utility>
|
|
24
|
+
|
|
25
|
+
#if defined(__APPLE__) || defined(__ANDROID__)
|
|
26
|
+
#include <pthread.h>
|
|
27
|
+
#endif
|
|
28
|
+
|
|
29
|
+
namespace qntx {
|
|
30
|
+
|
|
31
|
+
namespace jsi = facebook::jsi;
|
|
32
|
+
|
|
33
|
+
namespace {
|
|
34
|
+
|
|
35
|
+
// Turns the current exception into a description. Called only inside catch
|
|
36
|
+
// blocks so the exception_ptr — and any jsi values a JSError holds — die
|
|
37
|
+
// while the runtime that owns them is still alive.
|
|
38
|
+
std::string describe(const std::exception_ptr& error) {
|
|
39
|
+
try {
|
|
40
|
+
std::rethrow_exception(error);
|
|
41
|
+
} catch (const jsi::JSError& e) {
|
|
42
|
+
std::string message = e.getMessage();
|
|
43
|
+
const std::string& stack = e.getStack();
|
|
44
|
+
if (!stack.empty()) {
|
|
45
|
+
message += "\n" + stack;
|
|
46
|
+
}
|
|
47
|
+
return "jsi::JSError: " + message;
|
|
48
|
+
} catch (const std::exception& e) {
|
|
49
|
+
return std::string(e.what());
|
|
50
|
+
} catch (...) {
|
|
51
|
+
return "unknown error";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
hermes::vm::RuntimeConfig runtimeConfigFor(KernelProfile profile) {
|
|
56
|
+
auto builder = profile == KernelProfile::hardened
|
|
57
|
+
? facebook::hermes::hardenedHermesRuntimeConfig().rebuild()
|
|
58
|
+
: hermes::vm::RuntimeConfig::Builder();
|
|
59
|
+
return builder.withMicrotaskQueue(true)
|
|
60
|
+
.withEnableEval(false)
|
|
61
|
+
.withAsyncBreakCheckInEval(true)
|
|
62
|
+
.build();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
void nameKernelThread() {
|
|
66
|
+
#if defined(__APPLE__)
|
|
67
|
+
pthread_setname_np("qntx-kernel");
|
|
68
|
+
#elif defined(__ANDROID__)
|
|
69
|
+
pthread_setname_np(pthread_self(), "qntx-kernel");
|
|
70
|
+
#endif
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
} // namespace
|
|
74
|
+
|
|
75
|
+
struct KernelRuntime::Impl {
|
|
76
|
+
KernelScript script;
|
|
77
|
+
Hooks hooks;
|
|
78
|
+
KernelProfile profile = KernelProfile::standard;
|
|
79
|
+
std::shared_ptr<KernelGlobalsState> globals;
|
|
80
|
+
|
|
81
|
+
// Guards `immediate`, and the `stopping` store in destroy() so the loop's
|
|
82
|
+
// cv predicate can never miss a wakeup.
|
|
83
|
+
std::mutex mutex;
|
|
84
|
+
std::condition_variable cv;
|
|
85
|
+
std::deque<std::function<void()>> immediate;
|
|
86
|
+
|
|
87
|
+
// Inbound frame deliveries queued but not yet run; capped at
|
|
88
|
+
// kMaxInboundFrames — the kernel must keep up or the host exits integrity.
|
|
89
|
+
static constexpr size_t kMaxInboundFrames = 64;
|
|
90
|
+
size_t pendingPosts = 0;
|
|
91
|
+
|
|
92
|
+
std::atomic_bool stopping{false};
|
|
93
|
+
|
|
94
|
+
// Publishes the live runtime's IHermes so destroy() can interrupt running
|
|
95
|
+
// JS. The kernel thread sets it after makeHermesRuntime and withdraws it
|
|
96
|
+
// under the same mutex BEFORE rt.reset(), so asyncTriggerTimeout can never
|
|
97
|
+
// touch a destroyed runtime.
|
|
98
|
+
std::mutex interruptMutex;
|
|
99
|
+
facebook::hermes::IHermes* interruptable = nullptr;
|
|
100
|
+
|
|
101
|
+
std::mutex stoppedMutex;
|
|
102
|
+
std::condition_variable stoppedCv;
|
|
103
|
+
bool stopped = false;
|
|
104
|
+
|
|
105
|
+
// Kernel thread only.
|
|
106
|
+
std::unique_ptr<jsi::Runtime> rt;
|
|
107
|
+
std::optional<std::string> fatal;
|
|
108
|
+
struct Timer {
|
|
109
|
+
uint64_t id;
|
|
110
|
+
jsi::Function fn;
|
|
111
|
+
};
|
|
112
|
+
using TimedMap = std::multimap<std::chrono::steady_clock::time_point, Timer>;
|
|
113
|
+
TimedMap timed;
|
|
114
|
+
std::unordered_map<uint64_t, TimedMap::iterator> timerIndex;
|
|
115
|
+
uint64_t nextTimerId = 0;
|
|
116
|
+
|
|
117
|
+
// Read and written only under `mutex`: enqueue() may run on the KDF worker
|
|
118
|
+
// thread while teardown() runs on the kernel thread.
|
|
119
|
+
bool tornDown = false;
|
|
120
|
+
|
|
121
|
+
std::atomic_bool running{false};
|
|
122
|
+
|
|
123
|
+
void run();
|
|
124
|
+
void initRuntime();
|
|
125
|
+
void evaluate();
|
|
126
|
+
void loop();
|
|
127
|
+
void fail(std::string message);
|
|
128
|
+
void teardown();
|
|
129
|
+
void deliver(std::vector<uint8_t> bytes);
|
|
130
|
+
// Queues a task that runs fn(rt) on the kernel thread. Accepts work even
|
|
131
|
+
// after `stopping`: queued-but-unrun tasks are destroyed by teardown, which
|
|
132
|
+
// runs before the runtime itself is destroyed — so jsi values captured by
|
|
133
|
+
// tasks are always released on the kernel thread while the runtime lives.
|
|
134
|
+
void enqueue(std::function<void(jsi::Runtime&)> task);
|
|
135
|
+
KernelGlobalsState::TimerCancel scheduleTimer(
|
|
136
|
+
std::chrono::steady_clock::time_point deadline,
|
|
137
|
+
jsi::Function fn);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
KernelRuntime::KernelRuntime() : impl_(std::make_shared<Impl>()) {}
|
|
141
|
+
|
|
142
|
+
KernelRuntime::~KernelRuntime() {
|
|
143
|
+
destroy();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
std::shared_ptr<KernelRuntime> KernelRuntime::start(
|
|
147
|
+
KernelScript script,
|
|
148
|
+
Hooks hooks,
|
|
149
|
+
std::string vaultDirectory,
|
|
150
|
+
KernelProfile profile) {
|
|
151
|
+
if (script.bytes == nullptr || script.bytes->empty()) {
|
|
152
|
+
throw std::invalid_argument("KernelRuntime::start: empty script");
|
|
153
|
+
}
|
|
154
|
+
auto self = std::shared_ptr<KernelRuntime>(new KernelRuntime());
|
|
155
|
+
auto& impl = *self->impl_;
|
|
156
|
+
impl.profile = profile;
|
|
157
|
+
impl.script = std::move(script);
|
|
158
|
+
impl.hooks = std::move(hooks);
|
|
159
|
+
impl.globals = std::make_shared<KernelGlobalsState>();
|
|
160
|
+
impl.globals->vaultDirectory = std::move(vaultDirectory);
|
|
161
|
+
impl.globals->active = std::make_shared<std::atomic_bool>(true);
|
|
162
|
+
impl.globals->postToHost = impl.hooks.onPost;
|
|
163
|
+
impl.globals->reportFatal = [&impl](std::string message) {
|
|
164
|
+
impl.fatal = std::move(message);
|
|
165
|
+
};
|
|
166
|
+
impl.globals->scheduleTimer =
|
|
167
|
+
[&impl](std::chrono::steady_clock::time_point deadline, jsi::Function fn) {
|
|
168
|
+
return impl.scheduleTimer(deadline, std::move(fn));
|
|
169
|
+
};
|
|
170
|
+
// Async completions (e.g. pbkdf2) may arrive after the KernelRuntime object
|
|
171
|
+
// is destroyed, so reach the impl through a weak_ptr instead of a reference.
|
|
172
|
+
std::weak_ptr<Impl> weakImpl = self->impl_;
|
|
173
|
+
impl.globals->enqueueTask =
|
|
174
|
+
[weakImpl](std::function<void(jsi::Runtime&)> task) mutable {
|
|
175
|
+
if (auto impl = weakImpl.lock()) {
|
|
176
|
+
impl->enqueue(std::move(task));
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
impl.running.store(true, std::memory_order_release);
|
|
180
|
+
// The detached thread holds the Impl, not the KernelRuntime: dropping the
|
|
181
|
+
// last public handle just requests shutdown through ~KernelRuntime →
|
|
182
|
+
// destroy(), and the kernel thread finishes teardown on its own.
|
|
183
|
+
std::thread([impl = self->impl_]() { impl->run(); }).detach();
|
|
184
|
+
return self;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
bool KernelRuntime::running() const noexcept {
|
|
188
|
+
return impl_->running.load(std::memory_order_acquire);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
bool KernelRuntime::waitUntilStopped(std::chrono::milliseconds timeout) const {
|
|
192
|
+
auto& impl = *impl_;
|
|
193
|
+
std::unique_lock<std::mutex> lock(impl.stoppedMutex);
|
|
194
|
+
return impl.stoppedCv.wait_for(lock, timeout, [&] { return impl.stopped; });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
void KernelRuntime::post(std::vector<uint8_t> bytes) {
|
|
198
|
+
auto& impl = *impl_;
|
|
199
|
+
std::function<void()> integrity;
|
|
200
|
+
{
|
|
201
|
+
std::lock_guard<std::mutex> lock(impl.mutex);
|
|
202
|
+
if (impl.stopping.load(std::memory_order_acquire) ||
|
|
203
|
+
!impl.running.load(std::memory_order_acquire)) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (impl.pendingPosts >= Impl::kMaxInboundFrames) {
|
|
207
|
+
integrity = impl.hooks.onIntegrity;
|
|
208
|
+
} else {
|
|
209
|
+
impl.pendingPosts += 1;
|
|
210
|
+
impl.immediate.push_back([&impl, bytes = std::move(bytes)]() mutable {
|
|
211
|
+
{
|
|
212
|
+
std::lock_guard<std::mutex> inner(impl.mutex);
|
|
213
|
+
impl.pendingPosts -= 1;
|
|
214
|
+
}
|
|
215
|
+
impl.deliver(std::move(bytes));
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
impl.cv.notify_one();
|
|
220
|
+
if (integrity) {
|
|
221
|
+
integrity();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
void KernelRuntime::destroy() noexcept {
|
|
226
|
+
auto& impl = *impl_;
|
|
227
|
+
{
|
|
228
|
+
std::lock_guard<std::mutex> lock(impl.mutex);
|
|
229
|
+
if (impl.stopping.load(std::memory_order_acquire)) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
impl.stopping.store(true, std::memory_order_release);
|
|
233
|
+
}
|
|
234
|
+
impl.cv.notify_all();
|
|
235
|
+
{
|
|
236
|
+
std::lock_guard<std::mutex> lock(impl.interruptMutex);
|
|
237
|
+
if (impl.interruptable != nullptr) {
|
|
238
|
+
impl.interruptable->asyncTriggerTimeout();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
void KernelRuntime::Impl::run() {
|
|
244
|
+
nameKernelThread();
|
|
245
|
+
auto work = [this] {
|
|
246
|
+
std::optional<std::string> error;
|
|
247
|
+
try {
|
|
248
|
+
initRuntime();
|
|
249
|
+
loop();
|
|
250
|
+
} catch (...) {
|
|
251
|
+
// describe() runs inside the catch so a JSError (and the jsi values it
|
|
252
|
+
// holds) is destroyed while the runtime still exists.
|
|
253
|
+
try {
|
|
254
|
+
error = describe(std::current_exception());
|
|
255
|
+
} catch (...) {
|
|
256
|
+
error = "undescribable kernel error";
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// An exception raised after a destroy() request — the async interrupt or
|
|
260
|
+
// anything racing it — is a normal stop, not a kernel failure.
|
|
261
|
+
if (error.has_value() && !stopping.load(std::memory_order_acquire)) {
|
|
262
|
+
fail(std::move(*error));
|
|
263
|
+
}
|
|
264
|
+
teardown();
|
|
265
|
+
};
|
|
266
|
+
#if defined(__ANDROID__)
|
|
267
|
+
// Run the kernel body inside a Java call frame via fbjni. A plain
|
|
268
|
+
// AttachCurrentThread gives the thread a JNIEnv but FindClass still resolves
|
|
269
|
+
// through the boot class loader, which cannot see app-dex classes — Hermes
|
|
270
|
+
// delegates String.prototype.normalize/localeCompare to
|
|
271
|
+
// com.facebook.hermes.unicode.AndroidUnicodeUtils, and Nitro's registry is a
|
|
272
|
+
// Java class; the former throws fbjni::JniException across a noexcept
|
|
273
|
+
// boundary (std::terminate) on a raw attached thread. WithClassLoader makes
|
|
274
|
+
// app classes resolvable and attaches the thread for the whole duration.
|
|
275
|
+
facebook::jni::ThreadScope::WithClassLoader(std::move(work));
|
|
276
|
+
#else
|
|
277
|
+
work();
|
|
278
|
+
#endif
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
void KernelRuntime::Impl::initRuntime() {
|
|
282
|
+
rt = facebook::hermes::makeHermesRuntime(runtimeConfigFor(profile));
|
|
283
|
+
{
|
|
284
|
+
std::lock_guard<std::mutex> lock(interruptMutex);
|
|
285
|
+
interruptable = jsi::castInterface<facebook::hermes::IHermes>(rt.get());
|
|
286
|
+
}
|
|
287
|
+
if (interruptable == nullptr) {
|
|
288
|
+
throw std::runtime_error("hermes runtime does not expose IHermes");
|
|
289
|
+
}
|
|
290
|
+
if (stopping.load(std::memory_order_acquire)) {
|
|
291
|
+
// destroy() won the race before the script ran: skip globals and
|
|
292
|
+
// evaluation; run() then returns from loop() at once into teardown.
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
installKernelGlobals(*rt, globals);
|
|
297
|
+
|
|
298
|
+
if (hooks.beforeEvaluate) {
|
|
299
|
+
hooks.beforeEvaluate(*rt);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
evaluate();
|
|
303
|
+
rt->drainMicrotasks();
|
|
304
|
+
if (fatal.has_value()) {
|
|
305
|
+
throw std::runtime_error(*fatal);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
void KernelRuntime::Impl::evaluate() {
|
|
310
|
+
const auto& bytes = *script.bytes;
|
|
311
|
+
if (script.kind == KernelScript::Kind::bytecode) {
|
|
312
|
+
auto* api = jsi::castInterface<facebook::hermes::IHermesRootAPI>(
|
|
313
|
+
facebook::hermes::makeHermesRootAPI());
|
|
314
|
+
if (api == nullptr) {
|
|
315
|
+
throw std::runtime_error("Hermes root API unavailable");
|
|
316
|
+
}
|
|
317
|
+
if (!api->isHermesBytecode(bytes.data(), bytes.size())) {
|
|
318
|
+
throw std::runtime_error("embedded code is not Hermes bytecode");
|
|
319
|
+
}
|
|
320
|
+
std::string sanityError;
|
|
321
|
+
if (!api->hermesBytecodeSanityCheck(bytes.data(), bytes.size(), &sanityError)) {
|
|
322
|
+
throw std::runtime_error(
|
|
323
|
+
"embedded Hermes bytecode failed sanity check (version " +
|
|
324
|
+
std::to_string(api->getBytecodeVersion()) + "): " + sanityError);
|
|
325
|
+
}
|
|
326
|
+
rt->evaluateJavaScript(
|
|
327
|
+
std::make_shared<VectorBuffer>(script.bytes), "kernel.hbc");
|
|
328
|
+
} else {
|
|
329
|
+
rt->evaluateJavaScript(
|
|
330
|
+
std::make_shared<jsi::StringBuffer>(
|
|
331
|
+
std::string(reinterpret_cast<const char*>(bytes.data()), bytes.size())),
|
|
332
|
+
"kernel.ts");
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
void KernelRuntime::Impl::loop() {
|
|
337
|
+
for (;;) {
|
|
338
|
+
std::function<void()> task;
|
|
339
|
+
std::optional<Timer> due;
|
|
340
|
+
{
|
|
341
|
+
std::unique_lock<std::mutex> lock(mutex);
|
|
342
|
+
for (;;) {
|
|
343
|
+
if (stopping.load(std::memory_order_acquire)) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const auto now = std::chrono::steady_clock::now();
|
|
347
|
+
// Timers live on the kernel thread only; the mutex here just happens
|
|
348
|
+
// to be held while picking the next piece of work.
|
|
349
|
+
if (!timed.empty() && timed.begin()->first <= now) {
|
|
350
|
+
auto node = timed.extract(timed.begin());
|
|
351
|
+
timerIndex.erase(node.mapped().id);
|
|
352
|
+
due = std::move(node.mapped());
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
if (!immediate.empty()) {
|
|
356
|
+
task = std::move(immediate.front());
|
|
357
|
+
immediate.pop_front();
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
if (timed.empty()) {
|
|
361
|
+
cv.wait(lock);
|
|
362
|
+
} else {
|
|
363
|
+
cv.wait_until(lock, timed.begin()->first);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
std::optional<std::string> error;
|
|
369
|
+
try {
|
|
370
|
+
if (task) {
|
|
371
|
+
task();
|
|
372
|
+
} else if (due.has_value()) {
|
|
373
|
+
due->fn.call(*rt);
|
|
374
|
+
}
|
|
375
|
+
rt->drainMicrotasks();
|
|
376
|
+
} catch (...) {
|
|
377
|
+
error = describe(std::current_exception());
|
|
378
|
+
}
|
|
379
|
+
// Drop jsi values while the runtime is alive, whichever way we exit.
|
|
380
|
+
due.reset();
|
|
381
|
+
task = nullptr;
|
|
382
|
+
if (error.has_value()) {
|
|
383
|
+
if (stopping.load(std::memory_order_acquire)) {
|
|
384
|
+
// The destroy() interrupt surfaced as an exception — a normal stop.
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
fail("kernel task failed: " + std::move(*error));
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (fatal.has_value()) {
|
|
391
|
+
fail(std::move(*fatal));
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
void KernelRuntime::Impl::deliver(std::vector<uint8_t> bytes) {
|
|
398
|
+
if (!globals->messageHandler.has_value()) {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
auto buffer = rt->createArrayBuffer(std::make_shared<OwningBuffer>(std::move(bytes)));
|
|
402
|
+
globals->messageHandler->call(*rt, jsi::Value(std::move(buffer)));
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
void KernelRuntime::Impl::enqueue(std::function<void(jsi::Runtime&)> task) {
|
|
406
|
+
{
|
|
407
|
+
std::lock_guard<std::mutex> lock(mutex);
|
|
408
|
+
if (tornDown) {
|
|
409
|
+
// The loop is gone and teardown already ran: drop the task here so its
|
|
410
|
+
// captures (wiping buffers, dead pending calls) release immediately.
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
immediate.push_back(
|
|
414
|
+
[this, task = std::move(task)]() mutable { task(*rt); });
|
|
415
|
+
}
|
|
416
|
+
cv.notify_one();
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
KernelGlobalsState::TimerCancel KernelRuntime::Impl::scheduleTimer(
|
|
420
|
+
std::chrono::steady_clock::time_point deadline,
|
|
421
|
+
jsi::Function fn) {
|
|
422
|
+
// Kernel thread only — host functions and the loop run there, so no lock.
|
|
423
|
+
const uint64_t id = nextTimerId++;
|
|
424
|
+
auto it = timed.emplace(deadline, Timer{id, std::move(fn)});
|
|
425
|
+
timerIndex.emplace(id, it);
|
|
426
|
+
return [this, id] {
|
|
427
|
+
if (tornDown) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
auto found = timerIndex.find(id);
|
|
431
|
+
if (found != timerIndex.end()) {
|
|
432
|
+
// Erasing releases the stored jsi::Function immediately instead of
|
|
433
|
+
// parking it until the deadline.
|
|
434
|
+
timed.erase(found->second);
|
|
435
|
+
timerIndex.erase(found);
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
void KernelRuntime::Impl::fail(std::string message) {
|
|
441
|
+
// Kernel thread only.
|
|
442
|
+
stopping.store(true, std::memory_order_release);
|
|
443
|
+
if (hooks.onError) {
|
|
444
|
+
try {
|
|
445
|
+
hooks.onError(std::move(message));
|
|
446
|
+
} catch (...) {
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
teardown();
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
void KernelRuntime::Impl::teardown() {
|
|
453
|
+
// Kernel thread only; tornDown is shared with enqueue() on other threads,
|
|
454
|
+
// so it is set under the same mutex that guards the queue.
|
|
455
|
+
{
|
|
456
|
+
std::lock_guard<std::mutex> lock(mutex);
|
|
457
|
+
if (tornDown) {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
tornDown = true;
|
|
461
|
+
// Queued-but-unrun tasks die here, on the kernel thread, while the
|
|
462
|
+
// runtime is alive; enqueue() drops anything that arrives after this.
|
|
463
|
+
immediate.clear();
|
|
464
|
+
}
|
|
465
|
+
// Drop every jsi value before the runtime itself.
|
|
466
|
+
globals->intrinsics.reset();
|
|
467
|
+
globals->messageHandler.reset();
|
|
468
|
+
globals->integrityHandler.reset();
|
|
469
|
+
for (const auto& call : globals->pending) {
|
|
470
|
+
call->done.reset();
|
|
471
|
+
}
|
|
472
|
+
globals->pending.clear();
|
|
473
|
+
// A KDF worker that sees inactive drops its job — safe only because `done`
|
|
474
|
+
// was already cleared above, on this thread, while the runtime was alive.
|
|
475
|
+
globals->active->store(false, std::memory_order_release);
|
|
476
|
+
// A system prompt still on screen belongs to nobody now: dismiss it.
|
|
477
|
+
for (const auto& cancel : globals->cancelCeremonies) {
|
|
478
|
+
cancel();
|
|
479
|
+
}
|
|
480
|
+
// Timed/timerIndex are kernel-thread-only state.
|
|
481
|
+
timed.clear();
|
|
482
|
+
timerIndex.clear();
|
|
483
|
+
{
|
|
484
|
+
std::lock_guard<std::mutex> lock(interruptMutex);
|
|
485
|
+
interruptable = nullptr;
|
|
486
|
+
}
|
|
487
|
+
rt.reset();
|
|
488
|
+
running.store(false, std::memory_order_release);
|
|
489
|
+
{
|
|
490
|
+
std::lock_guard<std::mutex> lock(stoppedMutex);
|
|
491
|
+
stopped = true;
|
|
492
|
+
}
|
|
493
|
+
stoppedCv.notify_all();
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
} // namespace qntx
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <hermes/hermes.h>
|
|
4
|
+
#include <jsi/jsi.h>
|
|
5
|
+
|
|
6
|
+
#include <chrono>
|
|
7
|
+
#include <cstdint>
|
|
8
|
+
#include <functional>
|
|
9
|
+
#include <memory>
|
|
10
|
+
#include <string>
|
|
11
|
+
#include <vector>
|
|
12
|
+
|
|
13
|
+
namespace qntx {
|
|
14
|
+
|
|
15
|
+
// Hermes hardening level for the kernel runtime. `standard` runs a plain
|
|
16
|
+
// HermesVM config; `hardened` starts from hermes' hardenedHermesRuntimeConfig()
|
|
17
|
+
// (untrusted-code profile). Both disable eval, enable the runtime-owned
|
|
18
|
+
// microtask queue, and emit async break checks in eval-compiled code.
|
|
19
|
+
enum class KernelProfile { standard, hardened };
|
|
20
|
+
|
|
21
|
+
// Immutable script payload: bundled kernel source or compiled Hermes bytecode.
|
|
22
|
+
// Bytecode must be built with hermesc -emit-async-break-check so destroy() can
|
|
23
|
+
// interrupt running JS.
|
|
24
|
+
struct KernelScript {
|
|
25
|
+
enum class Kind { source, bytecode };
|
|
26
|
+
Kind kind;
|
|
27
|
+
std::shared_ptr<const std::vector<uint8_t>> bytes;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// A Hermes runtime running the wallet kernel on its own thread, driven by a
|
|
31
|
+
// task queue instead of the JS event loop.
|
|
32
|
+
//
|
|
33
|
+
// Lifecycle:
|
|
34
|
+
// - start() returns a shared_ptr; the kernel thread is detached and owns the
|
|
35
|
+
// shared state, so dropping the last KernelRuntime does not leak or hang.
|
|
36
|
+
// - destroy() requests shutdown: it is noexcept, idempotent, callable from
|
|
37
|
+
// any thread (including the kernel thread itself), and never blocks — it
|
|
38
|
+
// interrupts running JS via Hermes' async timeout. Destruction calls it.
|
|
39
|
+
// - running() is true from start() until teardown finishes on the kernel
|
|
40
|
+
// thread.
|
|
41
|
+
// - waitUntilStopped() blocks until teardown finished; it exists for tests
|
|
42
|
+
// and probes only — never call it on a UI thread.
|
|
43
|
+
//
|
|
44
|
+
// Host integration:
|
|
45
|
+
// - Hooks.beforeEvaluate runs on the kernel thread after globals install and
|
|
46
|
+
// before the script evaluates.
|
|
47
|
+
// - Hooks.onPost receives bytes the script posts via __qntx.post.
|
|
48
|
+
// - Hooks.onError receives a single fatal error (script exception, task
|
|
49
|
+
// exception, unhandled rejection); the runtime is torn down afterwards.
|
|
50
|
+
// Errors that only exist because shutdown was requested never reach it.
|
|
51
|
+
class KernelRuntime final : public std::enable_shared_from_this<KernelRuntime> {
|
|
52
|
+
public:
|
|
53
|
+
struct Hooks {
|
|
54
|
+
std::function<void(facebook::jsi::Runtime&)> beforeEvaluate;
|
|
55
|
+
std::function<void(std::vector<uint8_t>)> onPost;
|
|
56
|
+
std::function<void(std::string)> onError;
|
|
57
|
+
// Fired when post() exceeds the inbound frame queue cap (64) — a transport
|
|
58
|
+
// violation the host maps to an `integrity` exit, not a kernel crash.
|
|
59
|
+
std::function<void()> onIntegrity;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// `vaultDirectory` is the directory the kernel's vault file lives in (the
|
|
63
|
+
// host resolves it via platformVaultDirectory); the __qntx.file capabilities
|
|
64
|
+
// capture it for kernelFileRead/kernelFileWrite.
|
|
65
|
+
static std::shared_ptr<KernelRuntime> start(
|
|
66
|
+
KernelScript script,
|
|
67
|
+
Hooks hooks,
|
|
68
|
+
std::string vaultDirectory,
|
|
69
|
+
KernelProfile profile = KernelProfile::standard);
|
|
70
|
+
|
|
71
|
+
~KernelRuntime();
|
|
72
|
+
|
|
73
|
+
KernelRuntime(const KernelRuntime&) = delete;
|
|
74
|
+
KernelRuntime& operator=(const KernelRuntime&) = delete;
|
|
75
|
+
KernelRuntime(KernelRuntime&&) = delete;
|
|
76
|
+
KernelRuntime& operator=(KernelRuntime&&) = delete;
|
|
77
|
+
|
|
78
|
+
// Delivers bytes to the script's message handler. Safe after destroy():
|
|
79
|
+
// returns immediately if the runtime is stopping or gone.
|
|
80
|
+
void post(std::vector<uint8_t> bytes);
|
|
81
|
+
|
|
82
|
+
void destroy() noexcept;
|
|
83
|
+
bool running() const noexcept;
|
|
84
|
+
bool waitUntilStopped(std::chrono::milliseconds timeout) const;
|
|
85
|
+
|
|
86
|
+
private:
|
|
87
|
+
struct Impl;
|
|
88
|
+
|
|
89
|
+
KernelRuntime();
|
|
90
|
+
|
|
91
|
+
std::shared_ptr<Impl> impl_;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
} // namespace qntx
|