From 62c409a4391c23b0f5a5f29173d43315d88581f2 Mon Sep 17 00:00:00 2001 From: Mateusz Jonak Date: Wed, 10 Dec 2025 22:00:03 +0100 Subject: [PATCH 01/19] feat: Add deterministic runtime support with disabled eval and function features This commit introduces a new deterministic runtime in QuickJS, allowing for controlled execution by disabling the 'eval' and 'Function' features. It includes functions to initialize the deterministic context and define properties for the host environment. The changes enhance the runtime's capabilities for deterministic execution scenarios. --- quickjs.c | 158 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ quickjs.h | 1 + 2 files changed, 159 insertions(+) diff --git a/quickjs.c b/quickjs.c index 6f461d69d..d9e00bff0 100644 --- a/quickjs.c +++ b/quickjs.c @@ -489,6 +489,8 @@ struct JSContext { const char *input, size_t input_len, const char *filename, int flags, int scope_idx); void *user_opaque; + + BOOL deterministic_mode; }; typedef union JSFloat64Union { @@ -2221,6 +2223,158 @@ JSContext *JS_NewContext(JSRuntime *rt) return ctx; } +enum { + JS_DETERMINISTIC_DISABLED_EVAL = 1, + JS_DETERMINISTIC_DISABLED_FUNCTION = 2, +}; + +static const char *js_get_disabled_name(int magic) +{ + switch (magic) { + case JS_DETERMINISTIC_DISABLED_FUNCTION: + return "Function"; + case JS_DETERMINISTIC_DISABLED_EVAL: + default: + return "eval"; + } +} + +static JSValue js_deterministic_disabled(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv, int magic) +{ + const char *name = js_get_disabled_name(magic); + return JS_ThrowTypeError(ctx, "%s is disabled in deterministic mode", name); +} + +static int js_deterministic_disable_eval(JSContext *ctx) +{ + JSValue fn; + int ret; + + fn = JS_NewCFunctionMagic(ctx, js_deterministic_disabled, "eval", 1, + JS_CFUNC_generic_magic, JS_DETERMINISTIC_DISABLED_EVAL); + if (JS_IsException(fn)) + return -1; + + ret = JS_DefinePropertyValue(ctx, ctx->global_obj, JS_ATOM_eval, JS_DupValue(ctx, fn), + JS_PROP_HAS_VALUE | JS_PROP_HAS_CONFIGURABLE | + JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE); + if (ret < 0) { + JS_FreeValue(ctx, fn); + return -1; + } + + JS_FreeValue(ctx, ctx->eval_obj); + ctx->eval_obj = JS_UNDEFINED; + JS_FreeValue(ctx, fn); + return 0; +} + +static int js_deterministic_disable_function(JSContext *ctx) +{ + JSValue fn; + int ret; + + fn = JS_NewCFunctionMagic(ctx, js_deterministic_disabled, "Function", 1, + JS_CFUNC_constructor_or_func_magic, JS_DETERMINISTIC_DISABLED_FUNCTION); + if (JS_IsException(fn)) + return -1; + + ret = JS_DefinePropertyValue(ctx, ctx->global_obj, JS_ATOM_Function, JS_DupValue(ctx, fn), + JS_PROP_HAS_VALUE | JS_PROP_HAS_CONFIGURABLE | + JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE); + JS_FreeValue(ctx, fn); + if (ret < 0) + return -1; + + return 0; +} + +static int js_deterministic_init_host(JSContext *ctx) +{ + JSValue host_ns, host_v1; + int ret; + + host_ns = JS_NewObjectProto(ctx, JS_NULL); + if (JS_IsException(host_ns)) + return -1; + + host_v1 = JS_NewObjectProto(ctx, JS_NULL); + if (JS_IsException(host_v1)) { + JS_FreeValue(ctx, host_ns); + return -1; + } + + ret = JS_DefinePropertyValueStr(ctx, host_ns, "v1", JS_DupValue(ctx, host_v1), + JS_PROP_HAS_VALUE | JS_PROP_HAS_CONFIGURABLE | + JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE); + if (ret < 0) + goto fail; + + ret = JS_DefinePropertyValueStr(ctx, ctx->global_obj, "Host", JS_DupValue(ctx, host_ns), + JS_PROP_HAS_VALUE | JS_PROP_HAS_CONFIGURABLE | + JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE); + if (ret < 0) + goto fail; + + JS_FreeValue(ctx, host_ns); + JS_FreeValue(ctx, host_v1); + + return 0; + +fail: + JS_FreeValue(ctx, host_ns); + JS_FreeValue(ctx, host_v1); + return -1; +} + +static int js_deterministic_init_context(JSContext *ctx) +{ + if (JS_AddIntrinsicBaseObjects(ctx) || + JS_AddIntrinsicEval(ctx) || + JS_AddIntrinsicJSON(ctx) || + JS_AddIntrinsicMapSet(ctx) || + js_deterministic_disable_eval(ctx) || + js_deterministic_disable_function(ctx) || + js_deterministic_init_host(ctx)) { + return -1; + } + ctx->deterministic_mode = TRUE; + return 0; +} + +int JS_NewDeterministicRuntime(JSRuntime **out_rt, JSContext **out_ctx) +{ + JSRuntime *rt; + JSContext *ctx; + + if (!out_rt || !out_ctx) + return -1; + + *out_rt = NULL; + *out_ctx = NULL; + + rt = JS_NewRuntime(); + if (!rt) + return -1; + + ctx = JS_NewContextRaw(rt); + if (!ctx) { + JS_FreeRuntime(rt); + return -1; + } + + if (js_deterministic_init_context(ctx)) { + JS_FreeContext(ctx); + JS_FreeRuntime(rt); + return -1; + } + + *out_rt = rt; + *out_ctx = ctx; + return 0; +} + void *JS_GetContextOpaque(JSContext *ctx) { return ctx->user_opaque; @@ -40435,6 +40589,10 @@ static JSValue js_function_proto(JSContext *ctx, JSValueConst this_val, static JSValue js_function_constructor(JSContext *ctx, JSValueConst new_target, int argc, JSValueConst *argv, int magic) { + if (unlikely(ctx->deterministic_mode)) { + return JS_ThrowTypeError(ctx, "Function constructor is disabled in deterministic mode"); + } + JSFunctionKindEnum func_kind = magic; int i, n, ret; JSValue s, proto, obj = JS_UNDEFINED; diff --git a/quickjs.h b/quickjs.h index 92cc000d0..766d36568 100644 --- a/quickjs.h +++ b/quickjs.h @@ -383,6 +383,7 @@ void JS_RunGC(JSRuntime *rt); JS_BOOL JS_IsLiveObject(JSRuntime *rt, JSValueConst obj); JSContext *JS_NewContext(JSRuntime *rt); +int JS_NewDeterministicRuntime(JSRuntime **out_rt, JSContext **out_ctx); void JS_FreeContext(JSContext *s); JSContext *JS_DupContext(JSContext *ctx); void *JS_GetContextOpaque(JSContext *ctx); From adac78bc30b82150d1e11895ea5a7cecf36bd878 Mon Sep 17 00:00:00 2001 From: Mateusz Jonak Date: Wed, 10 Dec 2025 23:35:13 +0100 Subject: [PATCH 02/19] feat: Add support for deterministic random function in QuickJS This commit extends the deterministic runtime by introducing a mechanism to disable the 'Math.random' function. It includes a new function to handle the disabling and updates the context initialization to incorporate this feature, enhancing the control over random number generation in deterministic execution scenarios. --- quickjs.c | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/quickjs.c b/quickjs.c index d9e00bff0..905d8e23c 100644 --- a/quickjs.c +++ b/quickjs.c @@ -2226,11 +2226,14 @@ JSContext *JS_NewContext(JSRuntime *rt) enum { JS_DETERMINISTIC_DISABLED_EVAL = 1, JS_DETERMINISTIC_DISABLED_FUNCTION = 2, + JS_DETERMINISTIC_DISABLED_RANDOM = 3, }; static const char *js_get_disabled_name(int magic) { switch (magic) { + case JS_DETERMINISTIC_DISABLED_RANDOM: + return "Math.random"; case JS_DETERMINISTIC_DISABLED_FUNCTION: return "Function"; case JS_DETERMINISTIC_DISABLED_EVAL: @@ -2290,6 +2293,38 @@ static int js_deterministic_disable_function(JSContext *ctx) return 0; } +static int js_deterministic_disable_random(JSContext *ctx) +{ + JSValue math, fn; + int ret; + + math = JS_GetProperty(ctx, ctx->global_obj, JS_ATOM_Math); + if (JS_IsException(math)) + return -1; + if (!JS_IsObject(math)) { + JS_FreeValue(ctx, math); + return -1; + } + + fn = JS_NewCFunctionMagic(ctx, js_deterministic_disabled, "random", 0, + JS_CFUNC_generic_magic, JS_DETERMINISTIC_DISABLED_RANDOM); + if (JS_IsException(fn)) { + JS_FreeValue(ctx, math); + return -1; + } + + ret = JS_DefinePropertyValueStr(ctx, math, "random", JS_DupValue(ctx, fn), + JS_PROP_HAS_VALUE | JS_PROP_HAS_CONFIGURABLE | + JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE); + + JS_FreeValue(ctx, fn); + JS_FreeValue(ctx, math); + if (ret < 0) + return -1; + + return 0; +} + static int js_deterministic_init_host(JSContext *ctx) { JSValue host_ns, host_v1; @@ -2336,9 +2371,11 @@ static int js_deterministic_init_context(JSContext *ctx) JS_AddIntrinsicMapSet(ctx) || js_deterministic_disable_eval(ctx) || js_deterministic_disable_function(ctx) || + js_deterministic_disable_random(ctx) || js_deterministic_init_host(ctx)) { return -1; } + ctx->random_state = 1; /* deterministic seed */ ctx->deterministic_mode = TRUE; return 0; } From 7ad043a4cd68c9d6b048a6ee659ff484c6018c03 Mon Sep 17 00:00:00 2001 From: Mateusz Jonak Date: Wed, 10 Dec 2025 23:40:38 +0100 Subject: [PATCH 03/19] feat: Add support for disabling Promise in deterministic runtime This commit introduces a new feature to the deterministic runtime in QuickJS, allowing the 'Promise' constructor to be disabled. It includes a function to handle the disabling and updates the context initialization to incorporate this feature, enhancing control over asynchronous operations in deterministic execution scenarios. --- quickjs.c | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/quickjs.c b/quickjs.c index 905d8e23c..dca0621c9 100644 --- a/quickjs.c +++ b/quickjs.c @@ -2227,11 +2227,14 @@ enum { JS_DETERMINISTIC_DISABLED_EVAL = 1, JS_DETERMINISTIC_DISABLED_FUNCTION = 2, JS_DETERMINISTIC_DISABLED_RANDOM = 3, + JS_DETERMINISTIC_DISABLED_PROMISE = 4, }; static const char *js_get_disabled_name(int magic) { switch (magic) { + case JS_DETERMINISTIC_DISABLED_PROMISE: + return "Promise"; case JS_DETERMINISTIC_DISABLED_RANDOM: return "Math.random"; case JS_DETERMINISTIC_DISABLED_FUNCTION: @@ -2325,6 +2328,51 @@ static int js_deterministic_disable_random(JSContext *ctx) return 0; } +static int js_deterministic_disable_promise(JSContext *ctx) +{ + JSValue fn; + int ret; + + fn = JS_NewCFunctionMagic(ctx, js_deterministic_disabled, "Promise", 1, + JS_CFUNC_constructor_or_func_magic, JS_DETERMINISTIC_DISABLED_PROMISE); + if (JS_IsException(fn)) + return -1; + + ret = JS_DefinePropertyValue(ctx, ctx->global_obj, JS_ATOM_Promise, JS_DupValue(ctx, fn), + JS_PROP_HAS_VALUE | JS_PROP_HAS_CONFIGURABLE | + JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE); + + /* Ensure async internals see the disabled ctor */ + JS_FreeValue(ctx, ctx->promise_ctor); + ctx->promise_ctor = JS_DupValue(ctx, fn); + + /* Mirror common Promise statics to the same disabled stub */ + JS_DefinePropertyValueStr(ctx, fn, "resolve", JS_DupValue(ctx, fn), + JS_PROP_HAS_VALUE | JS_PROP_HAS_CONFIGURABLE | + JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE); + JS_DefinePropertyValueStr(ctx, fn, "reject", JS_DupValue(ctx, fn), + JS_PROP_HAS_VALUE | JS_PROP_HAS_CONFIGURABLE | + JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE); + JS_DefinePropertyValueStr(ctx, fn, "all", JS_DupValue(ctx, fn), + JS_PROP_HAS_VALUE | JS_PROP_HAS_CONFIGURABLE | + JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE); + JS_DefinePropertyValueStr(ctx, fn, "race", JS_DupValue(ctx, fn), + JS_PROP_HAS_VALUE | JS_PROP_HAS_CONFIGURABLE | + JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE); + JS_DefinePropertyValueStr(ctx, fn, "any", JS_DupValue(ctx, fn), + JS_PROP_HAS_VALUE | JS_PROP_HAS_CONFIGURABLE | + JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE); + JS_DefinePropertyValueStr(ctx, fn, "allSettled", JS_DupValue(ctx, fn), + JS_PROP_HAS_VALUE | JS_PROP_HAS_CONFIGURABLE | + JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE); + + JS_FreeValue(ctx, fn); + if (ret < 0) + return -1; + + return 0; +} + static int js_deterministic_init_host(JSContext *ctx) { JSValue host_ns, host_v1; @@ -2372,6 +2420,7 @@ static int js_deterministic_init_context(JSContext *ctx) js_deterministic_disable_eval(ctx) || js_deterministic_disable_function(ctx) || js_deterministic_disable_random(ctx) || + js_deterministic_disable_promise(ctx) || js_deterministic_init_host(ctx)) { return -1; } From c9947cef419b8958c5f45047f0803c79297a6087 Mon Sep 17 00:00:00 2001 From: Mateusz Jonak Date: Wed, 10 Dec 2025 23:50:48 +0100 Subject: [PATCH 04/19] feat: Add support for disabling RegExp and Proxy in deterministic runtime This commit enhances the deterministic runtime in QuickJS by introducing the ability to disable the 'RegExp' and 'Proxy' constructors. It includes functions to handle the disabling of these features and updates the context initialization accordingly, providing greater control over the execution environment in deterministic scenarios. --- quickjs.c | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/quickjs.c b/quickjs.c index dca0621c9..438e8f30c 100644 --- a/quickjs.c +++ b/quickjs.c @@ -2228,11 +2228,17 @@ enum { JS_DETERMINISTIC_DISABLED_FUNCTION = 2, JS_DETERMINISTIC_DISABLED_RANDOM = 3, JS_DETERMINISTIC_DISABLED_PROMISE = 4, + JS_DETERMINISTIC_DISABLED_REGEXP = 5, + JS_DETERMINISTIC_DISABLED_PROXY = 6, }; static const char *js_get_disabled_name(int magic) { switch (magic) { + case JS_DETERMINISTIC_DISABLED_PROXY: + return "Proxy"; + case JS_DETERMINISTIC_DISABLED_REGEXP: + return "RegExp"; case JS_DETERMINISTIC_DISABLED_PROMISE: return "Promise"; case JS_DETERMINISTIC_DISABLED_RANDOM: @@ -2252,6 +2258,14 @@ static JSValue js_deterministic_disabled(JSContext *ctx, JSValueConst this_val, return JS_ThrowTypeError(ctx, "%s is disabled in deterministic mode", name); } +static JSValue js_deterministic_compile_regexp(JSContext *ctx, JSValueConst pattern, + JSValueConst flags) +{ + (void)pattern; + (void)flags; + return JS_ThrowTypeError(ctx, "RegExp is disabled in deterministic mode"); +} + static int js_deterministic_disable_eval(JSContext *ctx) { JSValue fn; @@ -2328,6 +2342,52 @@ static int js_deterministic_disable_random(JSContext *ctx) return 0; } +static int js_deterministic_disable_regexp(JSContext *ctx) +{ + JSValue fn; + int ret; + + fn = JS_NewCFunctionMagic(ctx, js_deterministic_disabled, "RegExp", 2, + JS_CFUNC_constructor_or_func_magic, JS_DETERMINISTIC_DISABLED_REGEXP); + if (JS_IsException(fn)) + return -1; + + ret = JS_DefinePropertyValue(ctx, ctx->global_obj, JS_ATOM_RegExp, JS_DupValue(ctx, fn), + JS_PROP_HAS_VALUE | JS_PROP_HAS_CONFIGURABLE | + JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE); + if (ret < 0) { + JS_FreeValue(ctx, fn); + return -1; + } + + JS_FreeValue(ctx, ctx->regexp_ctor); + ctx->regexp_ctor = JS_DupValue(ctx, fn); + ctx->compile_regexp = js_deterministic_compile_regexp; + + JS_FreeValue(ctx, fn); + return 0; +} + +static int js_deterministic_disable_proxy(JSContext *ctx) +{ + JSValue fn; + int ret; + + fn = JS_NewCFunctionMagic(ctx, js_deterministic_disabled, "Proxy", 2, + JS_CFUNC_constructor_or_func_magic, JS_DETERMINISTIC_DISABLED_PROXY); + if (JS_IsException(fn)) + return -1; + + ret = JS_DefinePropertyValue(ctx, ctx->global_obj, JS_ATOM_Proxy, JS_DupValue(ctx, fn), + JS_PROP_HAS_VALUE | JS_PROP_HAS_CONFIGURABLE | + JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE); + JS_FreeValue(ctx, fn); + if (ret < 0) + return -1; + + return 0; +} + static int js_deterministic_disable_promise(JSContext *ctx) { JSValue fn; @@ -2419,6 +2479,8 @@ static int js_deterministic_init_context(JSContext *ctx) JS_AddIntrinsicMapSet(ctx) || js_deterministic_disable_eval(ctx) || js_deterministic_disable_function(ctx) || + js_deterministic_disable_regexp(ctx) || + js_deterministic_disable_proxy(ctx) || js_deterministic_disable_random(ctx) || js_deterministic_disable_promise(ctx) || js_deterministic_init_host(ctx)) { From 3122759a0c20dc0617619074b8f0d358f8e507a6 Mon Sep 17 00:00:00 2001 From: Mateusz Jonak Date: Thu, 11 Dec 2025 00:04:15 +0100 Subject: [PATCH 05/19] feat: Add support for disabling Typed Arrays, WebAssembly, and Atomics in deterministic runtime This commit enhances the deterministic runtime in QuickJS by introducing the ability to disable Typed Arrays, WebAssembly, and Atomics. It includes new functions to handle the disabling of these features and updates the context initialization accordingly, providing greater control over the execution environment in deterministic scenarios. --- quickjs.c | 88 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/quickjs.c b/quickjs.c index 438e8f30c..99466f22e 100644 --- a/quickjs.c +++ b/quickjs.c @@ -2230,11 +2230,29 @@ enum { JS_DETERMINISTIC_DISABLED_PROMISE = 4, JS_DETERMINISTIC_DISABLED_REGEXP = 5, JS_DETERMINISTIC_DISABLED_PROXY = 6, + JS_DETERMINISTIC_DISABLED_TYPED_ARRAY = 7, + JS_DETERMINISTIC_DISABLED_ARRAY_BUFFER = 8, + JS_DETERMINISTIC_DISABLED_SHARED_ARRAY_BUFFER = 9, + JS_DETERMINISTIC_DISABLED_DATAVIEW = 10, + JS_DETERMINISTIC_DISABLED_WEBASSEMBLY = 11, + JS_DETERMINISTIC_DISABLED_ATOMICS = 12, }; static const char *js_get_disabled_name(int magic) { switch (magic) { + case JS_DETERMINISTIC_DISABLED_ATOMICS: + return "Atomics"; + case JS_DETERMINISTIC_DISABLED_WEBASSEMBLY: + return "WebAssembly"; + case JS_DETERMINISTIC_DISABLED_DATAVIEW: + return "DataView"; + case JS_DETERMINISTIC_DISABLED_SHARED_ARRAY_BUFFER: + return "SharedArrayBuffer"; + case JS_DETERMINISTIC_DISABLED_ARRAY_BUFFER: + return "ArrayBuffer"; + case JS_DETERMINISTIC_DISABLED_TYPED_ARRAY: + return "Typed arrays"; case JS_DETERMINISTIC_DISABLED_PROXY: return "Proxy"; case JS_DETERMINISTIC_DISABLED_REGEXP: @@ -2255,9 +2273,32 @@ static JSValue js_deterministic_disabled(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic) { const char *name = js_get_disabled_name(magic); + if (magic == JS_DETERMINISTIC_DISABLED_TYPED_ARRAY) + return JS_ThrowTypeError(ctx, "%s are disabled in deterministic mode", name); return JS_ThrowTypeError(ctx, "%s is disabled in deterministic mode", name); } +static int js_deterministic_define_disabled_global(JSContext *ctx, const char *name, + int length, int magic) +{ + JSValue fn; + int ret; + + fn = JS_NewCFunctionMagic(ctx, js_deterministic_disabled, name, length, + JS_CFUNC_constructor_or_func_magic, magic); + if (JS_IsException(fn)) + return -1; + + ret = JS_DefinePropertyValueStr(ctx, ctx->global_obj, name, JS_DupValue(ctx, fn), + JS_PROP_HAS_VALUE | JS_PROP_HAS_CONFIGURABLE | + JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE); + JS_FreeValue(ctx, fn); + if (ret < 0) + return -1; + + return 0; +} + static JSValue js_deterministic_compile_regexp(JSContext *ctx, JSValueConst pattern, JSValueConst flags) { @@ -2433,6 +2474,50 @@ static int js_deterministic_disable_promise(JSContext *ctx) return 0; } +static int js_deterministic_disable_typed_arrays(JSContext *ctx) +{ + static const struct { + const char *name; + int length; + int magic; + } entries[] = { + { "ArrayBuffer", 1, JS_DETERMINISTIC_DISABLED_ARRAY_BUFFER }, + { "SharedArrayBuffer", 1, JS_DETERMINISTIC_DISABLED_SHARED_ARRAY_BUFFER }, + { "DataView", 3, JS_DETERMINISTIC_DISABLED_DATAVIEW }, + { "Uint8Array", 3, JS_DETERMINISTIC_DISABLED_TYPED_ARRAY }, + { "Uint8ClampedArray", 3, JS_DETERMINISTIC_DISABLED_TYPED_ARRAY }, + { "Int8Array", 3, JS_DETERMINISTIC_DISABLED_TYPED_ARRAY }, + { "Uint16Array", 3, JS_DETERMINISTIC_DISABLED_TYPED_ARRAY }, + { "Int16Array", 3, JS_DETERMINISTIC_DISABLED_TYPED_ARRAY }, + { "Uint32Array", 3, JS_DETERMINISTIC_DISABLED_TYPED_ARRAY }, + { "Int32Array", 3, JS_DETERMINISTIC_DISABLED_TYPED_ARRAY }, + { "BigInt64Array", 3, JS_DETERMINISTIC_DISABLED_TYPED_ARRAY }, + { "BigUint64Array", 3, JS_DETERMINISTIC_DISABLED_TYPED_ARRAY }, + { "Float16Array", 3, JS_DETERMINISTIC_DISABLED_TYPED_ARRAY }, + { "Float32Array", 3, JS_DETERMINISTIC_DISABLED_TYPED_ARRAY }, + { "Float64Array", 3, JS_DETERMINISTIC_DISABLED_TYPED_ARRAY }, + }; + + for (size_t i = 0; i < countof(entries); i++) { + if (js_deterministic_define_disabled_global(ctx, entries[i].name, + entries[i].length, entries[i].magic)) + return -1; + } + return 0; +} + +static int js_deterministic_disable_webassembly(JSContext *ctx) +{ + return js_deterministic_define_disabled_global(ctx, "WebAssembly", 1, + JS_DETERMINISTIC_DISABLED_WEBASSEMBLY); +} + +static int js_deterministic_disable_atomics(JSContext *ctx) +{ + return js_deterministic_define_disabled_global(ctx, "Atomics", 3, + JS_DETERMINISTIC_DISABLED_ATOMICS); +} + static int js_deterministic_init_host(JSContext *ctx) { JSValue host_ns, host_v1; @@ -2483,6 +2568,9 @@ static int js_deterministic_init_context(JSContext *ctx) js_deterministic_disable_proxy(ctx) || js_deterministic_disable_random(ctx) || js_deterministic_disable_promise(ctx) || + js_deterministic_disable_typed_arrays(ctx) || + js_deterministic_disable_atomics(ctx) || + js_deterministic_disable_webassembly(ctx) || js_deterministic_init_host(ctx)) { return -1; } From b2d34642b6c54f879c37870de347e015592a5012 Mon Sep 17 00:00:00 2001 From: Mateusz Jonak Date: Thu, 11 Dec 2025 00:06:44 +0100 Subject: [PATCH 06/19] feat: Add support for disabling console and print in deterministic runtime This commit enhances the deterministic runtime in QuickJS by introducing the ability to disable the 'console' and 'print' features. It includes new functions to handle the disabling of these features and updates the context initialization accordingly, providing greater control over the execution environment in deterministic scenarios. --- quickjs.c | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/quickjs.c b/quickjs.c index 99466f22e..7ab8406c6 100644 --- a/quickjs.c +++ b/quickjs.c @@ -2236,11 +2236,17 @@ enum { JS_DETERMINISTIC_DISABLED_DATAVIEW = 10, JS_DETERMINISTIC_DISABLED_WEBASSEMBLY = 11, JS_DETERMINISTIC_DISABLED_ATOMICS = 12, + JS_DETERMINISTIC_DISABLED_CONSOLE = 13, + JS_DETERMINISTIC_DISABLED_PRINT = 14, }; static const char *js_get_disabled_name(int magic) { switch (magic) { + case JS_DETERMINISTIC_DISABLED_PRINT: + return "print"; + case JS_DETERMINISTIC_DISABLED_CONSOLE: + return "console"; case JS_DETERMINISTIC_DISABLED_ATOMICS: return "Atomics"; case JS_DETERMINISTIC_DISABLED_WEBASSEMBLY: @@ -2518,6 +2524,49 @@ static int js_deterministic_disable_atomics(JSContext *ctx) JS_DETERMINISTIC_DISABLED_ATOMICS); } +static int js_deterministic_disable_console(JSContext *ctx) +{ + JSValue console; + + console = JS_NewObjectProto(ctx, JS_NULL); + if (JS_IsException(console)) + return -1; + + static const char *const methods[] = { "log", "info", "warn", "error", "debug" }; + for (size_t i = 0; i < countof(methods); i++) { + JSValue fn = JS_NewCFunctionMagic(ctx, js_deterministic_disabled, "console", 1, + JS_CFUNC_generic_magic, JS_DETERMINISTIC_DISABLED_CONSOLE); + if (JS_IsException(fn)) { + JS_FreeValue(ctx, console); + return -1; + } + if (JS_DefinePropertyValueStr(ctx, console, methods[i], JS_DupValue(ctx, fn), + JS_PROP_HAS_VALUE | JS_PROP_HAS_CONFIGURABLE | + JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE) < 0) { + JS_FreeValue(ctx, fn); + JS_FreeValue(ctx, console); + return -1; + } + JS_FreeValue(ctx, fn); + } + + if (JS_DefinePropertyValueStr(ctx, ctx->global_obj, "console", JS_DupValue(ctx, console), + JS_PROP_HAS_VALUE | JS_PROP_HAS_CONFIGURABLE | + JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE) < 0) { + JS_FreeValue(ctx, console); + return -1; + } + + JS_FreeValue(ctx, console); + return 0; +} + +static int js_deterministic_disable_print(JSContext *ctx) +{ + return js_deterministic_define_disabled_global(ctx, "print", 1, + JS_DETERMINISTIC_DISABLED_PRINT); +} + static int js_deterministic_init_host(JSContext *ctx) { JSValue host_ns, host_v1; @@ -2570,6 +2619,8 @@ static int js_deterministic_init_context(JSContext *ctx) js_deterministic_disable_promise(ctx) || js_deterministic_disable_typed_arrays(ctx) || js_deterministic_disable_atomics(ctx) || + js_deterministic_disable_console(ctx) || + js_deterministic_disable_print(ctx) || js_deterministic_disable_webassembly(ctx) || js_deterministic_init_host(ctx)) { return -1; From eb8f3eb11b07e06656ac65c4637dfb6c5e80cc15 Mon Sep 17 00:00:00 2001 From: Mateusz Jonak Date: Thu, 11 Dec 2025 00:13:57 +0100 Subject: [PATCH 07/19] feat: Add support for preventing extensions in deterministic runtime This commit enhances the deterministic runtime in QuickJS by introducing the ability to prevent extensions on host objects. It includes checks to ensure that extensions are disabled during context initialization, further improving control over the execution environment in deterministic scenarios. --- quickjs.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/quickjs.c b/quickjs.c index 7ab8406c6..f6cdb035e 100644 --- a/quickjs.c +++ b/quickjs.c @@ -2594,6 +2594,9 @@ static int js_deterministic_init_host(JSContext *ctx) if (ret < 0) goto fail; + if (JS_PreventExtensions(ctx, host_v1) < 0 || JS_PreventExtensions(ctx, host_ns) < 0) + goto fail; + JS_FreeValue(ctx, host_ns); JS_FreeValue(ctx, host_v1); From 5595afed795da3f142d7d9b124a6e2ddf26f9c1a Mon Sep 17 00:00:00 2001 From: Mateusz Jonak Date: Thu, 11 Dec 2025 09:52:57 +0100 Subject: [PATCH 08/19] feat: Introduce gas management in QuickJS runtime This commit adds a gas management system to the QuickJS runtime, allowing for resource usage tracking and control. It introduces new functions to set gas limits, check remaining gas, and handle out-of-gas scenarios. The changes enhance the deterministic runtime by providing mechanisms to prevent excessive resource consumption during execution, improving overall control in deterministic environments. --- quickjs.c | 234 +++++++++++++++++++++++++++++++++++++++++++++++++++++- quickjs.h | 8 ++ 2 files changed, 238 insertions(+), 4 deletions(-) diff --git a/quickjs.c b/quickjs.c index f6cdb035e..2ee5695ba 100644 --- a/quickjs.c +++ b/quickjs.c @@ -490,6 +490,10 @@ struct JSContext { const char *filename, int flags, int scope_idx); void *user_opaque; + uint64_t gas_limit; + uint64_t gas_remaining; + uint32_t gas_version; + BOOL deterministic_mode; }; @@ -1087,6 +1091,23 @@ enum OPCodeEnum { OP_TEMP_END, }; +static const uint16_t js_opcode_gas_cost[OP_COUNT] = { +#define FMT(f) +#define DEF(id, size, n_pop, n_push, f) [OP_ ## id] = 1, +#define def(id, size, n_pop, n_push, f) +#include "quickjs-opcode.h" +#undef def +#undef DEF +#undef FMT +}; + +static inline uint16_t js_get_opcode_gas_cost(uint8_t opcode) +{ + if (opcode < OP_COUNT) + return js_opcode_gas_cost[opcode]; + return 0; +} + static int JS_InitAtoms(JSRuntime *rt); static JSAtom __JS_NewAtomInit(JSRuntime *rt, const char *str, int len, int atom_type); @@ -2189,6 +2210,9 @@ JSContext *JS_NewContextRaw(JSRuntime *rt) ctx->iterator_ctor = JS_NULL; ctx->regexp_ctor = JS_NULL; ctx->promise_ctor = JS_NULL; + ctx->gas_limit = JS_GAS_UNLIMITED; + ctx->gas_remaining = JS_GAS_UNLIMITED; + ctx->gas_version = JS_GAS_VERSION_LATEST; init_list_head(&ctx->loaded_modules); if (JS_AddIntrinsicBasicObjects(ctx)) { @@ -2238,11 +2262,20 @@ enum { JS_DETERMINISTIC_DISABLED_ATOMICS = 12, JS_DETERMINISTIC_DISABLED_CONSOLE = 13, JS_DETERMINISTIC_DISABLED_PRINT = 14, + JS_DETERMINISTIC_DISABLED_JSON_PARSE = 15, + JS_DETERMINISTIC_DISABLED_JSON_STRINGIFY = 16, + JS_DETERMINISTIC_DISABLED_ARRAY_SORT = 17, }; static const char *js_get_disabled_name(int magic) { switch (magic) { + case JS_DETERMINISTIC_DISABLED_ARRAY_SORT: + return "Array.prototype.sort"; + case JS_DETERMINISTIC_DISABLED_JSON_STRINGIFY: + return "JSON.stringify"; + case JS_DETERMINISTIC_DISABLED_JSON_PARSE: + return "JSON.parse"; case JS_DETERMINISTIC_DISABLED_PRINT: return "print"; case JS_DETERMINISTIC_DISABLED_CONSOLE: @@ -2567,6 +2600,99 @@ static int js_deterministic_disable_print(JSContext *ctx) JS_DETERMINISTIC_DISABLED_PRINT); } +static int js_deterministic_disable_json(JSContext *ctx) +{ + JSValue json, parse_fn, stringify_fn; + int ret; + + json = JS_GetProperty(ctx, ctx->global_obj, JS_ATOM_JSON); + if (JS_IsException(json)) + return -1; + if (!JS_IsObject(json)) { + JS_FreeValue(ctx, json); + return -1; + } + + parse_fn = JS_NewCFunctionMagic(ctx, js_deterministic_disabled, "parse", 2, + JS_CFUNC_generic_magic, JS_DETERMINISTIC_DISABLED_JSON_PARSE); + if (JS_IsException(parse_fn)) { + JS_FreeValue(ctx, json); + return -1; + } + stringify_fn = JS_NewCFunctionMagic(ctx, js_deterministic_disabled, "stringify", 3, + JS_CFUNC_generic_magic, JS_DETERMINISTIC_DISABLED_JSON_STRINGIFY); + if (JS_IsException(stringify_fn)) { + JS_FreeValue(ctx, parse_fn); + JS_FreeValue(ctx, json); + return -1; + } + + ret = JS_DefinePropertyValueStr(ctx, json, "parse", JS_DupValue(ctx, parse_fn), + JS_PROP_HAS_VALUE | JS_PROP_HAS_CONFIGURABLE | + JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE); + if (ret < 0) + goto fail; + + ret = JS_DefinePropertyValueStr(ctx, json, "stringify", JS_DupValue(ctx, stringify_fn), + JS_PROP_HAS_VALUE | JS_PROP_HAS_CONFIGURABLE | + JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE); + if (ret < 0) + goto fail; + + JS_FreeValue(ctx, parse_fn); + JS_FreeValue(ctx, stringify_fn); + JS_FreeValue(ctx, json); + return 0; + +fail: + JS_FreeValue(ctx, parse_fn); + JS_FreeValue(ctx, stringify_fn); + JS_FreeValue(ctx, json); + return -1; +} + +static int js_deterministic_disable_array_sort(JSContext *ctx) +{ + JSValue array_ctor, array_proto, sort_fn; + int ret; + + array_ctor = JS_GetProperty(ctx, ctx->global_obj, JS_ATOM_Array); + if (JS_IsException(array_ctor)) + return -1; + if (!JS_IsObject(array_ctor)) { + JS_FreeValue(ctx, array_ctor); + return -1; + } + + array_proto = JS_GetProperty(ctx, array_ctor, JS_ATOM_prototype); + if (JS_IsException(array_proto)) { + JS_FreeValue(ctx, array_ctor); + return -1; + } + JS_FreeValue(ctx, array_ctor); + if (!JS_IsObject(array_proto)) { + JS_FreeValue(ctx, array_proto); + return -1; + } + + sort_fn = JS_NewCFunctionMagic(ctx, js_deterministic_disabled, "sort", 1, + JS_CFUNC_generic_magic, JS_DETERMINISTIC_DISABLED_ARRAY_SORT); + if (JS_IsException(sort_fn)) { + JS_FreeValue(ctx, array_proto); + return -1; + } + + ret = JS_DefinePropertyValueStr(ctx, array_proto, "sort", JS_DupValue(ctx, sort_fn), + JS_PROP_HAS_VALUE | JS_PROP_HAS_CONFIGURABLE | + JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE); + + JS_FreeValue(ctx, sort_fn); + JS_FreeValue(ctx, array_proto); + if (ret < 0) + return -1; + return 0; +} + static int js_deterministic_init_host(JSContext *ctx) { JSValue host_ns, host_v1; @@ -2624,6 +2750,8 @@ static int js_deterministic_init_context(JSContext *ctx) js_deterministic_disable_atomics(ctx) || js_deterministic_disable_console(ctx) || js_deterministic_disable_print(ctx) || + js_deterministic_disable_json(ctx) || + js_deterministic_disable_array_sort(ctx) || js_deterministic_disable_webassembly(ctx) || js_deterministic_init_host(ctx)) { return -1; @@ -2675,6 +2803,75 @@ void JS_SetContextOpaque(JSContext *ctx, void *opaque) ctx->user_opaque = opaque; } +static JSValue JS_ThrowOutOfGas(JSContext *ctx) +{ + JSValue obj, name, message, code; + + obj = JS_NewError(ctx); + if (JS_IsException(obj)) + return JS_EXCEPTION; + + name = JS_NewString(ctx, "OutOfGas"); + message = JS_NewString(ctx, "out of gas"); + code = JS_NewString(ctx, "OOG"); + if (JS_IsException(name) || JS_IsException(message) || JS_IsException(code)) { + if (!JS_IsException(name)) + JS_FreeValue(ctx, name); + if (!JS_IsException(message)) + JS_FreeValue(ctx, message); + if (!JS_IsException(code)) + JS_FreeValue(ctx, code); + JS_FreeValue(ctx, obj); + return JS_EXCEPTION; + } + + JS_DefinePropertyValue(ctx, obj, JS_ATOM_name, name, + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + JS_DefinePropertyValue(ctx, obj, JS_ATOM_message, message, + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + JS_DefinePropertyValueStr(ctx, obj, "code", code, + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + JS_Throw(ctx, obj); + JS_SetUncatchableException(ctx, TRUE); + return JS_EXCEPTION; +} + +void JS_SetGasLimit(JSContext *ctx, uint64_t gas_limit) +{ + ctx->gas_limit = gas_limit; + ctx->gas_remaining = gas_limit; +} + +uint64_t JS_GetGasRemaining(JSContext *ctx) +{ + return ctx->gas_remaining; +} + +uint64_t JS_GetGasLimit(JSContext *ctx) +{ + return ctx->gas_limit; +} + +uint32_t JS_GetGasVersion(JSContext *ctx) +{ + return ctx->gas_version; +} + +int JS_UseGas(JSContext *ctx, uint64_t amount) +{ + if (ctx->gas_limit == JS_GAS_UNLIMITED) + return 0; + if (amount == 0) + return 0; + if (amount > ctx->gas_remaining) { + ctx->gas_remaining = 0; + JS_ThrowOutOfGas(ctx); + return -1; + } + ctx->gas_remaining -= amount; + return 0; +} + /* set the new value and free the old value after (freeing the value can reallocate the object data) */ static inline void set_value(JSContext *ctx, JSValue *pval, JSValue new_val) @@ -17813,7 +18010,7 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj, size_t alloca_size; #if !DIRECT_DISPATCH -#define SWITCH(pc) switch (opcode = *pc++) +#define DISPATCH() switch (opcode) #define CASE(op) case op #define DEFAULT default #define BREAK break @@ -17828,10 +18025,10 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj, #include "quickjs-opcode.h" [ OP_COUNT ... 255 ] = &&case_default }; -#define SWITCH(pc) goto *dispatch_table[opcode = *pc++]; +#define DISPATCH() goto *dispatch_table[opcode] #define CASE(op) case_ ## op #define DEFAULT case_default -#define BREAK SWITCH(pc) +#define BREAK goto dispatch_next #endif if (js_poll_interrupts(caller_ctx)) @@ -17924,8 +18121,16 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj, for(;;) { int call_argc; JSValue *call_argv; + opcode = *pc++; + uint16_t gas_cost = js_get_opcode_gas_cost(opcode); + if (unlikely(JS_UseGas(ctx, gas_cost) != 0)) + goto exception; - SWITCH(pc) { + #if !DIRECT_DISPATCH + DISPATCH() { + #else + DISPATCH(); + #endif CASE(OP_push_i32): *sp++ = JS_NewInt32(ctx, get_u32(pc)); pc += 4; @@ -20505,7 +20710,13 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj, JS_ThrowInternalError(ctx, "invalid opcode: pc=%u opcode=0x%02x", (int)(pc - b->byte_code_buf - 1), opcode); goto exception; +#if !DIRECT_DISPATCH } +#endif +#if DIRECT_DISPATCH + dispatch_next: + ; +#endif } exception: if (is_backtrace_needed(ctx, rt->current_exception)) { @@ -41925,6 +42136,9 @@ static JSValue js_array_concat(JSContext *ctx, JSValueConst this_val, #define special_filter 4 #define special_TA 8 +#define JS_GAS_ARRAY_CB_BASE 5 +#define JS_GAS_ARRAY_CB_PER_ELEMENT 2 + static JSValue js_typed_array___speciesCreate(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv); @@ -41938,8 +42152,11 @@ static JSValue js_array_every(JSContext *ctx, JSValueConst this_val, int64_t len, k, n; int present; + obj = JS_UNDEFINED; ret = JS_UNDEFINED; val = JS_UNDEFINED; + if (unlikely(JS_UseGas(ctx, JS_GAS_ARRAY_CB_BASE) != 0)) + goto exception; if (special & special_TA) { obj = JS_DupValue(ctx, this_val); len = js_typed_array_get_length_unsafe(ctx, obj); @@ -41994,6 +42211,8 @@ static JSValue js_array_every(JSContext *ctx, JSValueConst this_val, n = 0; for(k = 0; k < len; k++) { + if (unlikely(JS_UseGas(ctx, JS_GAS_ARRAY_CB_PER_ELEMENT) != 0)) + goto exception; if (special & special_TA) { val = JS_GetPropertyInt64(ctx, obj, k); if (JS_IsException(val)) @@ -42095,8 +42314,11 @@ static JSValue js_array_reduce(JSContext *ctx, JSValueConst this_val, int64_t len, k, k1; int present; + obj = JS_UNDEFINED; acc = JS_UNDEFINED; val = JS_UNDEFINED; + if (unlikely(JS_UseGas(ctx, JS_GAS_ARRAY_CB_BASE) != 0)) + goto exception; if (special & special_TA) { obj = JS_DupValue(ctx, this_val); len = js_typed_array_get_length_unsafe(ctx, obj); @@ -42117,6 +42339,8 @@ static JSValue js_array_reduce(JSContext *ctx, JSValueConst this_val, acc = JS_DupValue(ctx, argv[1]); } else { for(;;) { + if (unlikely(JS_UseGas(ctx, JS_GAS_ARRAY_CB_PER_ELEMENT) != 0)) + goto exception; if (k >= len) { JS_ThrowTypeError(ctx, "empty array"); goto exception; @@ -42139,6 +42363,8 @@ static JSValue js_array_reduce(JSContext *ctx, JSValueConst this_val, } for (; k < len; k++) { k1 = (special & special_reduceRight) ? len - k - 1 : k; + if (unlikely(JS_UseGas(ctx, JS_GAS_ARRAY_CB_PER_ELEMENT) != 0)) + goto exception; if (special & special_TA) { val = JS_GetPropertyInt64(ctx, obj, k1); if (JS_IsException(val)) diff --git a/quickjs.h b/quickjs.h index 766d36568..afcebe58c 100644 --- a/quickjs.h +++ b/quickjs.h @@ -347,6 +347,9 @@ typedef JSValue JSCFunction(JSContext *ctx, JSValueConst this_val, int argc, JSV typedef JSValue JSCFunctionMagic(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic); typedef JSValue JSCFunctionData(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic, JSValue *func_data); +#define JS_GAS_VERSION_LATEST 1 +#define JS_GAS_UNLIMITED UINT64_C(0xffffffffffffffff) + typedef struct JSMallocState { size_t malloc_count; size_t malloc_size; @@ -388,6 +391,11 @@ void JS_FreeContext(JSContext *s); JSContext *JS_DupContext(JSContext *ctx); void *JS_GetContextOpaque(JSContext *ctx); void JS_SetContextOpaque(JSContext *ctx, void *opaque); +void JS_SetGasLimit(JSContext *ctx, uint64_t gas_limit); +uint64_t JS_GetGasRemaining(JSContext *ctx); +uint64_t JS_GetGasLimit(JSContext *ctx); +uint32_t JS_GetGasVersion(JSContext *ctx); +int JS_UseGas(JSContext *ctx, uint64_t amount); JSRuntime *JS_GetRuntime(JSContext *ctx); void JS_SetClassProto(JSContext *ctx, JSClassID class_id, JSValue obj); JSValue JS_GetClassProto(JSContext *ctx, JSClassID class_id); From ba10dc4328313eabed7bae2fe04f230ea2aaaa9f Mon Sep 17 00:00:00 2001 From: Mateusz Jonak Date: Thu, 11 Dec 2025 12:57:05 +0100 Subject: [PATCH 09/19] feat: Implement gas allocation tracking in memory management functions This commit enhances the QuickJS runtime by introducing gas allocation tracking in memory management functions. It adds a mechanism to charge gas for memory allocations, preventing excessive resource consumption. The changes include new functions for calculating gas costs and handling out-of-gas scenarios, improving control over resource usage in deterministic environments. --- quickjs.c | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 69 insertions(+), 6 deletions(-) diff --git a/quickjs.c b/quickjs.c index 2ee5695ba..a3efc3f88 100644 --- a/quickjs.c +++ b/quickjs.c @@ -274,6 +274,8 @@ struct JSRuntime { BOOL current_exception_is_uncatchable : 8; /* true if inside an out of memory error, to avoid recursing */ BOOL in_out_of_memory : 8; + /* true if inside out-of-gas handling to avoid recursion */ + BOOL in_out_of_gas : 8; struct JSStackFrame *current_stack_frame; @@ -1394,6 +1396,39 @@ static void js_trigger_gc(JSRuntime *rt, size_t size) } } +#define JS_GAS_ALLOC_BASE 3 +#define JS_GAS_ALLOC_PER_BYTE_SHIFT 4 + +static uint64_t js_gas_allocation_cost(size_t size) +{ + const uint64_t unit = UINT64_C(1) << JS_GAS_ALLOC_PER_BYTE_SHIFT; + uint64_t units; + + if (size == 0) { + units = 0; + } else if (size > UINT64_MAX - (unit - 1)) { + units = UINT64_MAX; + } else { + units = ((uint64_t)size + (unit - 1)) >> JS_GAS_ALLOC_PER_BYTE_SHIFT; + } + + if (units > UINT64_MAX - JS_GAS_ALLOC_BASE) + return UINT64_MAX; + return JS_GAS_ALLOC_BASE + units; +} + +static int js_charge_gas_allocation_ctx(JSContext *ctx, size_t size) +{ + JSRuntime *rt = ctx->rt; + + if (ctx->gas_limit == JS_GAS_UNLIMITED) + return 0; + if (rt->in_out_of_gas || rt->current_exception_is_uncatchable) + return 0; + + return JS_UseGas(ctx, js_gas_allocation_cost(size)); +} + static size_t js_malloc_usable_size_unknown(const void *ptr) { return 0; @@ -1432,9 +1467,12 @@ void *js_mallocz_rt(JSRuntime *rt, size_t size) void *js_malloc(JSContext *ctx, size_t size) { void *ptr; + if (size != 0 && js_charge_gas_allocation_ctx(ctx, size)) + return NULL; ptr = js_malloc_rt(ctx->rt, size); if (unlikely(!ptr)) { - JS_ThrowOutOfMemory(ctx); + if (JS_IsUninitialized(ctx->rt->current_exception)) + JS_ThrowOutOfMemory(ctx); return NULL; } return ptr; @@ -1444,9 +1482,12 @@ void *js_malloc(JSContext *ctx, size_t size) void *js_mallocz(JSContext *ctx, size_t size) { void *ptr; + if (size != 0 && js_charge_gas_allocation_ctx(ctx, size)) + return NULL; ptr = js_mallocz_rt(ctx->rt, size); if (unlikely(!ptr)) { - JS_ThrowOutOfMemory(ctx); + if (JS_IsUninitialized(ctx->rt->current_exception)) + JS_ThrowOutOfMemory(ctx); return NULL; } return ptr; @@ -1461,9 +1502,12 @@ void js_free(JSContext *ctx, void *ptr) void *js_realloc(JSContext *ctx, void *ptr, size_t size) { void *ret; + if (size != 0 && js_charge_gas_allocation_ctx(ctx, size)) + return NULL; ret = js_realloc_rt(ctx->rt, ptr, size); if (unlikely(!ret && size != 0)) { - JS_ThrowOutOfMemory(ctx); + if (JS_IsUninitialized(ctx->rt->current_exception)) + JS_ThrowOutOfMemory(ctx); return NULL; } return ret; @@ -1473,9 +1517,12 @@ void *js_realloc(JSContext *ctx, void *ptr, size_t size) void *js_realloc2(JSContext *ctx, void *ptr, size_t size, size_t *pslack) { void *ret; + if (size != 0 && js_charge_gas_allocation_ctx(ctx, size)) + return NULL; ret = js_realloc_rt(ctx->rt, ptr, size); if (unlikely(!ret && size != 0)) { - JS_ThrowOutOfMemory(ctx); + if (JS_IsUninitialized(ctx->rt->current_exception)) + JS_ThrowOutOfMemory(ctx); return NULL; } if (pslack) { @@ -1720,6 +1767,7 @@ JSRuntime *JS_NewRuntime2(const JSMallocFunctions *mf, void *opaque) JS_UpdateStackTop(rt); rt->current_exception = JS_UNINITIALIZED; + rt->in_out_of_gas = FALSE; return rt; fail: @@ -1968,6 +2016,11 @@ static JSString *js_alloc_string_rt(JSRuntime *rt, int max_len, int is_wide_char static JSString *js_alloc_string(JSContext *ctx, int max_len, int is_wide_char) { JSString *p; + size_t alloc_size = sizeof(JSString) + + (((size_t)max_len << is_wide_char) + 1 - is_wide_char); + + if (js_charge_gas_allocation_ctx(ctx, alloc_size)) + return NULL; p = js_alloc_string_rt(ctx->rt, max_len, is_wide_char); if (unlikely(!p)) { JS_ThrowOutOfMemory(ctx); @@ -2805,11 +2858,16 @@ void JS_SetContextOpaque(JSContext *ctx, void *opaque) static JSValue JS_ThrowOutOfGas(JSContext *ctx) { + JSRuntime *rt = ctx->rt; JSValue obj, name, message, code; + if (rt->in_out_of_gas) + return JS_EXCEPTION; + + rt->in_out_of_gas = TRUE; obj = JS_NewError(ctx); if (JS_IsException(obj)) - return JS_EXCEPTION; + goto fail; name = JS_NewString(ctx, "OutOfGas"); message = JS_NewString(ctx, "out of gas"); @@ -2822,7 +2880,7 @@ static JSValue JS_ThrowOutOfGas(JSContext *ctx) if (!JS_IsException(code)) JS_FreeValue(ctx, code); JS_FreeValue(ctx, obj); - return JS_EXCEPTION; + goto fail; } JS_DefinePropertyValue(ctx, obj, JS_ATOM_name, name, @@ -2833,6 +2891,11 @@ static JSValue JS_ThrowOutOfGas(JSContext *ctx) JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); JS_Throw(ctx, obj); JS_SetUncatchableException(ctx, TRUE); + rt->in_out_of_gas = FALSE; + return JS_EXCEPTION; + +fail: + rt->in_out_of_gas = FALSE; return JS_EXCEPTION; } From b4a584c794d471ef9a581cabd750aafd32b2f40d Mon Sep 17 00:00:00 2001 From: Mateusz Jonak Date: Thu, 11 Dec 2025 14:48:56 +0100 Subject: [PATCH 10/19] feat: Enhance deterministic runtime with garbage collection improvements This commit introduces new features to the QuickJS deterministic runtime, including support for a checkpoint-based garbage collection mechanism. It adds fields to track the state of deterministic garbage collection and modifies the garbage collection trigger to respect the deterministic mode. Additionally, a new function, JS_RunGCCheckpoint, is implemented to manage garbage collection in a controlled manner, improving resource management in deterministic environments. --- quickjs.c | 34 ++++++++++++++++++++++++++++++++++ quickjs.h | 1 + 2 files changed, 35 insertions(+) diff --git a/quickjs.c b/quickjs.c index a3efc3f88..1c80298d4 100644 --- a/quickjs.c +++ b/quickjs.c @@ -260,6 +260,9 @@ struct JSRuntime { struct list_head tmp_obj_list; /* used during GC */ JSGCPhaseEnum gc_phase : 8; size_t malloc_gc_threshold; + BOOL deterministic_mode : 8; + BOOL det_gc_pending : 8; + uint64_t det_gc_alloc_bytes; struct list_head weakref_list; /* list of JSWeakRefHeader.link */ #ifdef DUMP_LEAKS struct list_head string_list; /* list of JSString.link */ @@ -1379,6 +1382,9 @@ static JSClassID js_class_id_alloc = JS_CLASS_INIT_COUNT; static void js_trigger_gc(JSRuntime *rt, size_t size) { BOOL force_gc; + + if (rt->deterministic_mode) + return; #ifdef FORCE_GC_AT_MALLOC force_gc = TRUE; #else @@ -1398,6 +1404,7 @@ static void js_trigger_gc(JSRuntime *rt, size_t size) #define JS_GAS_ALLOC_BASE 3 #define JS_GAS_ALLOC_PER_BYTE_SHIFT 4 +#define JS_DET_GC_THRESHOLD_BYTES (512 * 1024) static uint64_t js_gas_allocation_cost(size_t size) { @@ -1426,6 +1433,12 @@ static int js_charge_gas_allocation_ctx(JSContext *ctx, size_t size) if (rt->in_out_of_gas || rt->current_exception_is_uncatchable) return 0; + if (rt->deterministic_mode) { + rt->det_gc_alloc_bytes += size; + if (rt->det_gc_alloc_bytes >= JS_DET_GC_THRESHOLD_BYTES) + rt->det_gc_pending = TRUE; + } + return JS_UseGas(ctx, js_gas_allocation_cost(size)); } @@ -2829,6 +2842,11 @@ int JS_NewDeterministicRuntime(JSRuntime **out_rt, JSContext **out_ctx) if (!rt) return -1; + rt->deterministic_mode = TRUE; + rt->det_gc_pending = FALSE; + rt->det_gc_alloc_bytes = 0; + JS_SetGCThreshold(rt, (size_t)-1); + ctx = JS_NewContextRaw(rt); if (!ctx) { JS_FreeRuntime(rt); @@ -2935,6 +2953,22 @@ int JS_UseGas(JSContext *ctx, uint64_t amount) return 0; } +int JS_RunGCCheckpoint(JSContext *ctx) +{ + JSRuntime *rt = ctx->rt; + + if (rt->current_exception_is_uncatchable || rt->in_out_of_gas) + return 0; + + if (rt->deterministic_mode && !rt->det_gc_pending) + return 0; + + JS_RunGC(rt); + rt->det_gc_pending = FALSE; + rt->det_gc_alloc_bytes = 0; + return 0; +} + /* set the new value and free the old value after (freeing the value can reallocate the object data) */ static inline void set_value(JSContext *ctx, JSValue *pval, JSValue new_val) diff --git a/quickjs.h b/quickjs.h index afcebe58c..a02751421 100644 --- a/quickjs.h +++ b/quickjs.h @@ -383,6 +383,7 @@ void JS_SetRuntimeOpaque(JSRuntime *rt, void *opaque); typedef void JS_MarkFunc(JSRuntime *rt, JSGCObjectHeader *gp); void JS_MarkValue(JSRuntime *rt, JSValueConst val, JS_MarkFunc *mark_func); void JS_RunGC(JSRuntime *rt); +int JS_RunGCCheckpoint(JSContext *ctx); JS_BOOL JS_IsLiveObject(JSRuntime *rt, JSValueConst obj); JSContext *JS_NewContext(JSRuntime *rt); From f8f443fc001e0b7087d3cf0b64b4f9df823c805c Mon Sep 17 00:00:00 2001 From: Mateusz Jonak Date: Thu, 11 Dec 2025 16:55:15 +0100 Subject: [PATCH 11/19] feat: Implement gas tracing functionality in QuickJS runtime This commit introduces a gas tracing system to the QuickJS runtime, allowing for detailed tracking of gas usage during execution. It adds a new structure, JSGasTraceData, to store gas usage metrics, and functions to enable, reset, and read gas trace data. The changes enhance resource management by providing insights into gas consumption for opcode execution, array callbacks, and memory allocations, improving control in deterministic environments. --- quickjs.c | 138 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- quickjs.h | 14 ++++++ 2 files changed, 149 insertions(+), 3 deletions(-) diff --git a/quickjs.c b/quickjs.c index 1c80298d4..6b052abfc 100644 --- a/quickjs.c +++ b/quickjs.c @@ -234,6 +234,7 @@ typedef enum { } JSGCPhaseEnum; typedef enum OPCodeEnum OPCodeEnum; +typedef struct JSGasTraceData JSGasTraceData; struct JSRuntime { JSMallocFunctions mf; @@ -499,6 +500,7 @@ struct JSContext { uint64_t gas_remaining; uint32_t gas_version; + JSGasTraceData *gas_trace; BOOL deterministic_mode; }; @@ -1113,6 +1115,77 @@ static inline uint16_t js_get_opcode_gas_cost(uint8_t opcode) return 0; } +struct JSGasTraceData { + BOOL enabled; + uint64_t opcode_count_total; + uint64_t opcode_gas; + uint64_t builtin_array_cb_base_count; + uint64_t builtin_array_cb_base_gas; + uint64_t builtin_array_cb_per_element_count; + uint64_t builtin_array_cb_per_element_gas; + uint64_t allocation_count; + uint64_t allocation_bytes; + uint64_t allocation_gas; +}; + +static void js_gas_trace_reset_counts(JSGasTraceData *trace) +{ + BOOL enabled = trace->enabled; + memset(trace, 0, sizeof(*trace)); + trace->enabled = enabled; +} + +static JSGasTraceData *js_gas_trace_or_null(JSContext *ctx) +{ + if (!ctx || !ctx->gas_trace || !ctx->gas_trace->enabled) + return NULL; + return ctx->gas_trace; +} + +static JSGasTraceData *js_gas_trace_ensure(JSContext *ctx) +{ + if (!ctx->gas_trace) { + ctx->gas_trace = js_mallocz_rt(ctx->rt, sizeof(JSGasTraceData)); + } + return ctx->gas_trace; +} + +static void js_gas_trace_record_opcode(JSContext *ctx, uint8_t opcode, uint16_t gas_cost) +{ + JSGasTraceData *trace = js_gas_trace_or_null(ctx); + if (!trace) + return; + + trace->opcode_count_total++; + trace->opcode_gas += gas_cost; +} + +static void js_gas_trace_record_array_cb(JSContext *ctx, uint16_t gas_cost, BOOL per_element) +{ + JSGasTraceData *trace = js_gas_trace_or_null(ctx); + if (!trace) + return; + + if (per_element) { + trace->builtin_array_cb_per_element_count++; + trace->builtin_array_cb_per_element_gas += gas_cost; + } else { + trace->builtin_array_cb_base_count++; + trace->builtin_array_cb_base_gas += gas_cost; + } +} + +static void js_gas_trace_record_allocation(JSContext *ctx, size_t size, uint64_t gas_cost) +{ + JSGasTraceData *trace = js_gas_trace_or_null(ctx); + if (!trace) + return; + + trace->allocation_count++; + trace->allocation_bytes += size; + trace->allocation_gas += gas_cost; +} + static int JS_InitAtoms(JSRuntime *rt); static JSAtom __JS_NewAtomInit(JSRuntime *rt, const char *str, int len, int atom_type); @@ -1427,9 +1500,8 @@ static uint64_t js_gas_allocation_cost(size_t size) static int js_charge_gas_allocation_ctx(JSContext *ctx, size_t size) { JSRuntime *rt = ctx->rt; + uint64_t gas_cost; - if (ctx->gas_limit == JS_GAS_UNLIMITED) - return 0; if (rt->in_out_of_gas || rt->current_exception_is_uncatchable) return 0; @@ -1439,7 +1511,12 @@ static int js_charge_gas_allocation_ctx(JSContext *ctx, size_t size) rt->det_gc_pending = TRUE; } - return JS_UseGas(ctx, js_gas_allocation_cost(size)); + gas_cost = js_gas_allocation_cost(size); + if (JS_UseGas(ctx, gas_cost)) + return -1; + + js_gas_trace_record_allocation(ctx, size, gas_cost); + return 0; } static size_t js_malloc_usable_size_unknown(const void *ptr) @@ -2938,6 +3015,53 @@ uint32_t JS_GetGasVersion(JSContext *ctx) return ctx->gas_version; } +int JS_EnableGasTrace(JSContext *ctx, int enabled) +{ + JSGasTraceData *trace; + + if (!ctx) + return -1; + + trace = js_gas_trace_ensure(ctx); + if (!trace) + return -1; + + memset(trace, 0, sizeof(*trace)); + trace->enabled = enabled ? TRUE : FALSE; + return 0; +} + +int JS_ResetGasTrace(JSContext *ctx) +{ + JSGasTraceData *trace = js_gas_trace_or_null(ctx); + + if (!trace) + return -1; + + js_gas_trace_reset_counts(trace); + return 0; +} + +int JS_ReadGasTrace(JSContext *ctx, JSGasTrace *out_trace) +{ + JSGasTraceData *trace = js_gas_trace_or_null(ctx); + + if (!trace || !out_trace) + return -1; + + out_trace->opcode_count = trace->opcode_count_total; + out_trace->opcode_gas = trace->opcode_gas; + out_trace->builtin_array_cb_base_count = trace->builtin_array_cb_base_count; + out_trace->builtin_array_cb_base_gas = trace->builtin_array_cb_base_gas; + out_trace->builtin_array_cb_per_element_count = trace->builtin_array_cb_per_element_count; + out_trace->builtin_array_cb_per_element_gas = trace->builtin_array_cb_per_element_gas; + out_trace->allocation_count = trace->allocation_count; + out_trace->allocation_bytes = trace->allocation_bytes; + out_trace->allocation_gas = trace->allocation_gas; + + return 0; +} + int JS_UseGas(JSContext *ctx, uint64_t amount) { if (ctx->gas_limit == JS_GAS_UNLIMITED) @@ -3140,6 +3264,8 @@ void JS_FreeContext(JSContext *ctx) list_del(&ctx->link); remove_gc_object(&ctx->header); + if (ctx->gas_trace) + js_free_rt(ctx->rt, ctx->gas_trace); js_free_rt(ctx->rt, ctx); } @@ -18222,6 +18348,7 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj, uint16_t gas_cost = js_get_opcode_gas_cost(opcode); if (unlikely(JS_UseGas(ctx, gas_cost) != 0)) goto exception; + js_gas_trace_record_opcode(ctx, opcode, gas_cost); #if !DIRECT_DISPATCH DISPATCH() { @@ -42254,6 +42381,7 @@ static JSValue js_array_every(JSContext *ctx, JSValueConst this_val, val = JS_UNDEFINED; if (unlikely(JS_UseGas(ctx, JS_GAS_ARRAY_CB_BASE) != 0)) goto exception; + js_gas_trace_record_array_cb(ctx, JS_GAS_ARRAY_CB_BASE, FALSE); if (special & special_TA) { obj = JS_DupValue(ctx, this_val); len = js_typed_array_get_length_unsafe(ctx, obj); @@ -42310,6 +42438,7 @@ static JSValue js_array_every(JSContext *ctx, JSValueConst this_val, for(k = 0; k < len; k++) { if (unlikely(JS_UseGas(ctx, JS_GAS_ARRAY_CB_PER_ELEMENT) != 0)) goto exception; + js_gas_trace_record_array_cb(ctx, JS_GAS_ARRAY_CB_PER_ELEMENT, TRUE); if (special & special_TA) { val = JS_GetPropertyInt64(ctx, obj, k); if (JS_IsException(val)) @@ -42416,6 +42545,7 @@ static JSValue js_array_reduce(JSContext *ctx, JSValueConst this_val, val = JS_UNDEFINED; if (unlikely(JS_UseGas(ctx, JS_GAS_ARRAY_CB_BASE) != 0)) goto exception; + js_gas_trace_record_array_cb(ctx, JS_GAS_ARRAY_CB_BASE, FALSE); if (special & special_TA) { obj = JS_DupValue(ctx, this_val); len = js_typed_array_get_length_unsafe(ctx, obj); @@ -42438,6 +42568,7 @@ static JSValue js_array_reduce(JSContext *ctx, JSValueConst this_val, for(;;) { if (unlikely(JS_UseGas(ctx, JS_GAS_ARRAY_CB_PER_ELEMENT) != 0)) goto exception; + js_gas_trace_record_array_cb(ctx, JS_GAS_ARRAY_CB_PER_ELEMENT, TRUE); if (k >= len) { JS_ThrowTypeError(ctx, "empty array"); goto exception; @@ -42462,6 +42593,7 @@ static JSValue js_array_reduce(JSContext *ctx, JSValueConst this_val, k1 = (special & special_reduceRight) ? len - k - 1 : k; if (unlikely(JS_UseGas(ctx, JS_GAS_ARRAY_CB_PER_ELEMENT) != 0)) goto exception; + js_gas_trace_record_array_cb(ctx, JS_GAS_ARRAY_CB_PER_ELEMENT, TRUE); if (special & special_TA) { val = JS_GetPropertyInt64(ctx, obj, k1); if (JS_IsException(val)) diff --git a/quickjs.h b/quickjs.h index a02751421..6d362b505 100644 --- a/quickjs.h +++ b/quickjs.h @@ -396,6 +396,20 @@ void JS_SetGasLimit(JSContext *ctx, uint64_t gas_limit); uint64_t JS_GetGasRemaining(JSContext *ctx); uint64_t JS_GetGasLimit(JSContext *ctx); uint32_t JS_GetGasVersion(JSContext *ctx); +typedef struct JSGasTrace { + uint64_t opcode_count; + uint64_t opcode_gas; + uint64_t builtin_array_cb_base_count; + uint64_t builtin_array_cb_base_gas; + uint64_t builtin_array_cb_per_element_count; + uint64_t builtin_array_cb_per_element_gas; + uint64_t allocation_count; + uint64_t allocation_bytes; + uint64_t allocation_gas; +} JSGasTrace; +int JS_EnableGasTrace(JSContext *ctx, int enabled); +int JS_ResetGasTrace(JSContext *ctx); +int JS_ReadGasTrace(JSContext *ctx, JSGasTrace *out_trace); int JS_UseGas(JSContext *ctx, uint64_t amount); JSRuntime *JS_GetRuntime(JSContext *ctx); void JS_SetClassProto(JSContext *ctx, JSClassID class_id, JSValue obj); From 821a066c322b7d75f91596a9a471318c3581d130 Mon Sep 17 00:00:00 2001 From: Mateusz Jonak Date: Fri, 12 Dec 2025 15:53:28 +0100 Subject: [PATCH 12/19] fix: Reset gas trace counts using dedicated function This commit updates the gas tracing functionality in the QuickJS runtime by replacing the direct memory reset with a call to the new function js_gas_trace_reset_counts. This change improves code clarity and ensures that gas trace counts are reset correctly, enhancing the overall reliability of the gas tracing system. --- quickjs.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quickjs.c b/quickjs.c index 6b052abfc..ac1493263 100644 --- a/quickjs.c +++ b/quickjs.c @@ -3026,7 +3026,7 @@ int JS_EnableGasTrace(JSContext *ctx, int enabled) if (!trace) return -1; - memset(trace, 0, sizeof(*trace)); + js_gas_trace_reset_counts(trace); trace->enabled = enabled ? TRUE : FALSE; return 0; } From 8f4f682d761a421c3ed22a66a319c7fe5426a58c Mon Sep 17 00:00:00 2001 From: Mateusz Jonak Date: Mon, 15 Dec 2025 18:16:50 +0100 Subject: [PATCH 13/19] feat(quickjs): add DV (canonical CBOR) encode/decode Add a Deterministic Values (DV) codec to the QuickJS fork. - Add quickjs-dv.c implementing JS_EncodeDV / JS_DecodeDV for a restricted CBOR subset (null/bool/finite numbers, UTF-8 text, arrays, plain objects as maps). - Enforce canonical encoding: shortest-length integers, float64-only (no non-finite), no indefinite lengths, canonical map key ordering, and duplicate key rejection. - Add public API types JSDvLimits/JSDvBuffer, defaults (JS_DV_LIMIT_DEFAULTS), and JS_FreeDVBuffer for output ownership cleanup. - Validate UTF-8 and normalize -0 to 0 for deterministic round-trips. --- quickjs-dv.c | 1087 ++++++++++++++++++++++++++++++++++++++++++++++++++ quickjs.h | 20 + 2 files changed, 1107 insertions(+) create mode 100644 quickjs-dv.c diff --git a/quickjs-dv.c b/quickjs-dv.c new file mode 100644 index 000000000..e82dad50d --- /dev/null +++ b/quickjs-dv.c @@ -0,0 +1,1087 @@ +#include "quickjs.h" +#include +#include +#include +#include +#include + +#ifndef JS_CLASS_OBJECT +/* Upstream class id for Object; used here to fetch the intrinsic prototype + without relying on the global Object binding. */ +#define JS_CLASS_OBJECT 1 +#endif + +/* Internal allocator hooks from quickjs.c */ +extern void *js_malloc(JSContext *ctx, size_t size); +extern void *js_realloc(JSContext *ctx, void *ptr, size_t size); +extern void js_free(JSContext *ctx, void *ptr); + +#define DV_CBOR_MAJOR_UINT 0 +#define DV_CBOR_MAJOR_NINT 1 +#define DV_CBOR_MAJOR_TEXT 3 +#define DV_CBOR_MAJOR_ARRAY 4 +#define DV_CBOR_MAJOR_MAP 5 +#define DV_CBOR_MAJOR_SIMPLE 7 + +static const int64_t dv_max_safe_int = 9007199254740991LL; /* 2^53 - 1 */ +static const int64_t dv_min_safe_int = -9007199254740991LL; /* -(2^53 - 1) */ + +static const JSDvLimits *dv_limits_or_default(const JSDvLimits *limits) { + return limits ? limits : &JS_DV_LIMIT_DEFAULTS; +} + +typedef struct { + JSContext *ctx; + uint8_t *data; + size_t size; + size_t capacity; + size_t max_size; +} JSDvBuilder; + +typedef struct { + const uint8_t *data; + size_t size; + size_t pos; + JSContext *ctx; +} JSDvReader; + +typedef struct { + JSAtom atom; + JSValue key_value; + uint8_t *encoded_key; + size_t encoded_key_len; +} JSDvKeyEntry; + +static int dv_throw(JSContext *ctx, const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + char buf[256]; + vsnprintf(buf, sizeof(buf), fmt, ap); + va_end(ap); + JS_ThrowTypeError(ctx, "%s", buf); + return -1; +} + +static int dv_builder_reserve(JSDvBuilder *builder, size_t additional) { + if (additional > builder->max_size - builder->size) { + JS_ThrowTypeError( + builder->ctx, + "DV encode: encoded DV exceeds maxEncodedBytes (%zu > %zu)", + builder->size + additional, + builder->max_size); + return -1; + } + + size_t required = builder->size + additional; + if (required <= builder->capacity) { + return 0; + } + + size_t new_capacity = builder->capacity ? builder->capacity : 64; + while (new_capacity < required) { + size_t next = new_capacity * 2; + if (next <= new_capacity) { + new_capacity = builder->max_size; + break; + } + new_capacity = next; + } + + if (new_capacity > builder->max_size) { + new_capacity = builder->max_size; + } + + uint8_t *reallocated = js_realloc(builder->ctx, builder->data, new_capacity); + if (!reallocated) { + return -1; + } + + builder->data = reallocated; + builder->capacity = new_capacity; + return 0; +} + +static void dv_builder_free(JSDvBuilder *builder) { + if (builder->data) { + js_free(builder->ctx, builder->data); + builder->data = NULL; + } + builder->size = 0; + builder->capacity = 0; +} + +static int dv_builder_push_u8(JSDvBuilder *builder, uint8_t value) { + if (dv_builder_reserve(builder, 1) != 0) { + return -1; + } + builder->data[builder->size++] = value; + return 0; +} + +static int dv_builder_push_bytes(JSDvBuilder *builder, const uint8_t *data, size_t length) { + if (length == 0) { + return 0; + } + if (dv_builder_reserve(builder, length) != 0) { + return -1; + } + memcpy(builder->data + builder->size, data, length); + builder->size += length; + return 0; +} + +static int dv_builder_push_u64_be(JSDvBuilder *builder, uint64_t value, size_t width) { + uint8_t buf[8]; + for (size_t i = 0; i < width; i++) { + buf[width - 1 - i] = (uint8_t)(value & 0xff); + value >>= 8; + } + return dv_builder_push_bytes(builder, buf, width); +} + +static int dv_validate_utf8(JSContext *ctx, const uint8_t *data, size_t length) { + size_t i = 0; + while (i < length) { + uint8_t byte = data[i]; + uint32_t codepoint; + size_t needed; + + if (byte < 0x80) { + codepoint = byte; + needed = 0; + } else if ((byte & 0xe0) == 0xc0) { + codepoint = byte & 0x1f; + needed = 1; + if (codepoint == 0) { + return dv_throw(ctx, "DV string contains invalid UTF-8"); + } + } else if ((byte & 0xf0) == 0xe0) { + codepoint = byte & 0x0f; + needed = 2; + } else if ((byte & 0xf8) == 0xf0) { + codepoint = byte & 0x07; + needed = 3; + } else { + return dv_throw(ctx, "DV string contains invalid UTF-8"); + } + + if (i + needed >= length) { + return dv_throw(ctx, "DV string contains invalid UTF-8"); + } + + for (size_t j = 0; j < needed; j++) { + uint8_t cont = data[i + 1 + j]; + if ((cont & 0xc0) != 0x80) { + return dv_throw(ctx, "DV string contains invalid UTF-8"); + } + codepoint = (codepoint << 6) | (cont & 0x3f); + } + + if ((needed == 1 && codepoint < 0x80) || + (needed == 2 && codepoint < 0x800) || + (needed == 3 && codepoint < 0x10000)) { + return dv_throw(ctx, "DV string contains invalid UTF-8"); + } + + if (codepoint > 0x10ffff) { + return dv_throw(ctx, "DV string contains invalid UTF-8"); + } + if (codepoint >= 0xd800 && codepoint <= 0xdfff) { + return dv_throw(ctx, "DV string contains lone surrogate code points"); + } + + i += needed + 1; + } + return 0; +} + +static int dv_encode_type_and_length(JSDvBuilder *builder, uint8_t major, uint64_t length) { + if (length <= 23) { + return dv_builder_push_u8(builder, (uint8_t)((major << 5) | length)); + } + if (length <= 0xff) { + if (dv_builder_push_u8(builder, (uint8_t)((major << 5) | 24)) != 0) { + return -1; + } + return dv_builder_push_u64_be(builder, length, 1); + } + if (length <= 0xffff) { + if (dv_builder_push_u8(builder, (uint8_t)((major << 5) | 25)) != 0) { + return -1; + } + return dv_builder_push_u64_be(builder, length, 2); + } + if (length <= 0xffffffff) { + if (dv_builder_push_u8(builder, (uint8_t)((major << 5) | 26)) != 0) { + return -1; + } + return dv_builder_push_u64_be(builder, length, 4); + } + if (dv_builder_push_u8(builder, (uint8_t)((major << 5) | 27)) != 0) { + return -1; + } + return dv_builder_push_u64_be(builder, length, 8); +} + +static int dv_encode_number(JSContext *ctx, double value, JSDvBuilder *builder) { + if (!isfinite(value)) { + return dv_throw(ctx, "DV numbers must be finite"); + } + + if (value == 0 && signbit(value)) { + value = 0; + } + + double int_part; + if (modf(value, &int_part) == 0.0) { + if (value > (double)dv_max_safe_int || value < (double)dv_min_safe_int) { + return dv_throw(ctx, + "integer is outside safe range (not in [%" PRId64 ", %" PRId64 "])", + dv_min_safe_int, + dv_max_safe_int); + } + + uint64_t unsigned_val; + if (value >= 0) { + unsigned_val = (uint64_t)value; + return dv_encode_type_and_length(builder, DV_CBOR_MAJOR_UINT, unsigned_val); + } + + unsigned_val = (uint64_t)(-1 - (int64_t)value); + return dv_encode_type_and_length(builder, DV_CBOR_MAJOR_NINT, unsigned_val); + } + + if (dv_builder_push_u8(builder, 0xfb) != 0) { + return -1; + } + union { + double d; + uint64_t u; + } u; + u.d = value; + return dv_builder_push_u64_be(builder, u.u, 8); +} + +static int dv_encode_string_bytes(JSContext *ctx, + JSValueConst value, + const JSDvLimits *limits, + uint8_t **out_bytes, + size_t *out_len) { + size_t byte_length = 0; + const char *raw = JS_ToCStringLen2(ctx, &byte_length, value, 0); + if (!raw) { + return -1; + } + + if (byte_length > limits->max_string_bytes) { + JS_FreeCString(ctx, raw); + return dv_throw(ctx, + "string exceeds maxStringBytes (%zu > %u)", + byte_length, + limits->max_string_bytes); + } + + if (dv_validate_utf8(ctx, (const uint8_t *)raw, byte_length) != 0) { + JS_FreeCString(ctx, raw); + return -1; + } + + uint8_t *copy = NULL; + if (byte_length > 0) { + copy = js_malloc(ctx, byte_length); + if (!copy) { + JS_FreeCString(ctx, raw); + return -1; + } + memcpy(copy, raw, byte_length); + } + JS_FreeCString(ctx, raw); + *out_bytes = copy; + *out_len = byte_length; + return 0; +} + +static int dv_encode_value(JSContext *ctx, + JSValueConst value, + const JSDvLimits *limits, + uint32_t depth, + JSDvBuilder *builder); + +static int dv_encode_array(JSContext *ctx, + JSValueConst value, + const JSDvLimits *limits, + uint32_t depth, + JSDvBuilder *builder) { + uint32_t next_depth = depth + 1; + if (next_depth > limits->max_depth) { + return dv_throw(ctx, "maxDepth %u exceeded", limits->max_depth); + } + + uint64_t length64 = 0; + JSValue length_val = JS_GetPropertyStr(ctx, value, "length"); + if (JS_IsException(length_val)) { + return -1; + } + int len_rc = JS_ToIndex(ctx, &length64, length_val); + JS_FreeValue(ctx, length_val); + if (len_rc < 0) { + return -1; + } + + if (length64 > limits->max_array_length) { + return dv_throw(ctx, + "array length exceeds maxArrayLength (%" PRIu64 " > %u)", + length64, + limits->max_array_length); + } + + uint32_t length = (uint32_t)length64; + if (dv_encode_type_and_length(builder, DV_CBOR_MAJOR_ARRAY, length) != 0) { + return -1; + } + + for (uint32_t i = 0; i < length; i++) { + JSValue element = JS_GetPropertyUint32(ctx, value, i); + if (JS_IsException(element)) { + return -1; + } + int rc = dv_encode_value(ctx, element, limits, next_depth, builder); + JS_FreeValue(ctx, element); + if (rc != 0) { + return -1; + } + } + + return 0; +} + +static int dv_compare_encoded_keys(const void *a, const void *b) { + const JSDvKeyEntry *ka = (const JSDvKeyEntry *)a; + const JSDvKeyEntry *kb = (const JSDvKeyEntry *)b; + + if (ka->encoded_key_len != kb->encoded_key_len) { + return ka->encoded_key_len < kb->encoded_key_len ? -1 : 1; + } + return memcmp(ka->encoded_key, kb->encoded_key, ka->encoded_key_len); +} + +static int dv_encode_object(JSContext *ctx, + JSValueConst value, + const JSDvLimits *limits, + uint32_t depth, + JSDvBuilder *builder) { + uint32_t next_depth = depth + 1; + if (next_depth > limits->max_depth) { + return dv_throw(ctx, "maxDepth %u exceeded", limits->max_depth); + } + + JSValue proto = JS_GetPrototype(ctx, value); + if (JS_IsException(proto)) { + return -1; + } + + int is_plain = 0; + if (JS_IsNull(proto)) { + is_plain = 1; + } else { + JSValue object_proto = JS_GetClassProto(ctx, JS_CLASS_OBJECT); + if (JS_IsException(object_proto)) { + JS_FreeValue(ctx, proto); + return -1; + } + + int same = JS_SameValue(ctx, proto, object_proto); + JS_FreeValue(ctx, object_proto); + if (same < 0) { + JS_FreeValue(ctx, proto); + return -1; + } + + is_plain = same; + } + + JS_FreeValue(ctx, proto); + + if (!is_plain) { + return dv_throw(ctx, "unsupported DV type: object"); + } + + JSPropertyEnum *props = NULL; + uint32_t prop_len = 0; + if (JS_GetOwnPropertyNames(ctx, + &props, + &prop_len, + value, + JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY) < 0) { + return -1; + } + + if (prop_len > limits->max_map_length) { + JS_FreePropertyEnum(ctx, props, prop_len); + return dv_throw(ctx, + "map entries exceed maxMapLength (%u > %u)", + prop_len, + limits->max_map_length); + } + + JSDvKeyEntry *entries = NULL; + if (prop_len > 0) { + entries = js_malloc(ctx, prop_len * sizeof(*entries)); + if (!entries) { + JS_FreePropertyEnum(ctx, props, prop_len); + return -1; + } + } + + for (uint32_t i = 0; i < prop_len; i++) { + entries[i].atom = props[i].atom; + entries[i].encoded_key = NULL; + entries[i].encoded_key_len = 0; + entries[i].key_value = JS_UNDEFINED; + + JSValue key_val = JS_AtomToString(ctx, props[i].atom); + if (JS_IsException(key_val)) { + goto encode_object_error; + } + + uint8_t *key_bytes = NULL; + size_t key_len = 0; + if (dv_encode_string_bytes(ctx, key_val, limits, &key_bytes, &key_len) != 0) { + JS_FreeValue(ctx, key_val); + goto encode_object_error; + } + + JSDvBuilder key_builder = { + .ctx = ctx, + .data = NULL, + .size = 0, + .capacity = 0, + .max_size = limits->max_encoded_bytes, + }; + + if (dv_encode_type_and_length(&key_builder, DV_CBOR_MAJOR_TEXT, key_len) != 0 || + dv_builder_push_bytes(&key_builder, key_bytes, key_len) != 0) { + dv_builder_free(&key_builder); + js_free(ctx, key_bytes); + JS_FreeValue(ctx, key_val); + goto encode_object_error; + } + + if (key_bytes) { + js_free(ctx, key_bytes); + } + + entries[i].encoded_key = key_builder.data; + entries[i].encoded_key_len = key_builder.size; + entries[i].key_value = key_val; + } + + qsort(entries, prop_len, sizeof(entries[0]), dv_compare_encoded_keys); + + for (uint32_t i = 1; i < prop_len; i++) { + int cmp = dv_compare_encoded_keys(&entries[i - 1], &entries[i]); + if (cmp == 0) { + JSValue str_val = JS_JSONStringify(ctx, + entries[i].key_value, + JS_UNDEFINED, + JS_UNDEFINED); + if (!JS_IsException(str_val)) { + const char *dup = JS_ToCString(ctx, str_val); + const char *dup_key = dup ? dup : ""; + dv_throw(ctx, "map contains duplicate key %s", dup_key); + if (dup) { + JS_FreeCString(ctx, dup); + } + JS_FreeValue(ctx, str_val); + } else { + dv_throw(ctx, "map contains duplicate key"); + } + goto encode_object_error; + } + } + + if (dv_encode_type_and_length(builder, DV_CBOR_MAJOR_MAP, prop_len) != 0) { + goto encode_object_error; + } + + for (uint32_t i = 0; i < prop_len; i++) { + if (dv_builder_push_bytes(builder, entries[i].encoded_key, entries[i].encoded_key_len) != 0) { + goto encode_object_error; + } + JSValue prop_val = JS_GetProperty(ctx, value, entries[i].atom); + if (JS_IsException(prop_val)) { + goto encode_object_error; + } + int rc = dv_encode_value(ctx, prop_val, limits, next_depth, builder); + JS_FreeValue(ctx, prop_val); + if (rc != 0) { + goto encode_object_error; + } + } + + for (uint32_t i = 0; i < prop_len; i++) { + if (entries[i].encoded_key) { + js_free(ctx, entries[i].encoded_key); + } + JS_FreeValue(ctx, entries[i].key_value); + } + if (entries) { + js_free(ctx, entries); + } + if (props) { + JS_FreePropertyEnum(ctx, props, prop_len); + } + return 0; + +encode_object_error: + if (entries) { + for (uint32_t i = 0; i < prop_len; i++) { + if (entries[i].encoded_key) { + js_free(ctx, entries[i].encoded_key); + } + if (!JS_IsUndefined(entries[i].key_value)) { + JS_FreeValue(ctx, entries[i].key_value); + } + } + js_free(ctx, entries); + } + if (props) { + JS_FreePropertyEnum(ctx, props, prop_len); + } + return -1; +} + +static int dv_encode_value(JSContext *ctx, + JSValueConst value, + const JSDvLimits *limits, + uint32_t depth, + JSDvBuilder *builder) { + if (depth > limits->max_depth) { + return dv_throw(ctx, "maxDepth %u exceeded", limits->max_depth); + } + + int tag = JS_VALUE_GET_NORM_TAG(value); + switch (tag) { + case JS_TAG_NULL: + return dv_builder_push_u8(builder, 0xf6); + case JS_TAG_BOOL: { + return dv_builder_push_u8(builder, JS_VALUE_GET_BOOL(value) ? 0xf5 : 0xf4); + } + case JS_TAG_INT: + return dv_encode_number(ctx, JS_VALUE_GET_INT(value), builder); + case JS_TAG_FLOAT64: + return dv_encode_number(ctx, JS_VALUE_GET_FLOAT64(value), builder); + case JS_TAG_STRING: + case JS_TAG_STRING_ROPE: { + uint8_t *bytes = NULL; + size_t len = 0; + if (dv_encode_string_bytes(ctx, value, limits, &bytes, &len) != 0) { + return -1; + } + int rc = dv_encode_type_and_length(builder, DV_CBOR_MAJOR_TEXT, len); + if (rc == 0) { + rc = dv_builder_push_bytes(builder, bytes, len); + } + if (bytes) { + js_free(ctx, bytes); + } + return rc; + } + case JS_TAG_OBJECT: { + int is_array = JS_IsArray(ctx, value); + if (is_array < 0) { + return -1; + } + if (is_array) { + return dv_encode_array(ctx, value, limits, depth, builder); + } + return dv_encode_object(ctx, value, limits, depth, builder); + } + default: + return dv_throw(ctx, "unsupported DV type: %d", tag); + } +} + +int JS_EncodeDV(JSContext *ctx, + JSValueConst value, + const JSDvLimits *maybe_limits, + JSDvBuffer *out_buffer) { + const JSDvLimits *limits = dv_limits_or_default(maybe_limits); + if (out_buffer) { + out_buffer->data = NULL; + out_buffer->length = 0; + } + + JSDvBuilder builder = { + .ctx = ctx, + .data = NULL, + .size = 0, + .capacity = 0, + .max_size = limits->max_encoded_bytes, + }; + + if (dv_encode_value(ctx, value, limits, 0, &builder) != 0) { + dv_builder_free(&builder); + return -1; + } + + if (out_buffer) { + out_buffer->data = builder.data; + out_buffer->length = builder.size; + } else { + dv_builder_free(&builder); + } + return 0; +} + +static int dv_reader_need(JSDvReader *reader, size_t amount) { + if (reader->pos + amount > reader->size) { + JS_ThrowTypeError(reader->ctx, "unexpected end of buffer"); + return -1; + } + return 0; +} + +static int dv_reader_read_u8(JSDvReader *reader, uint8_t *out) { + if (dv_reader_need(reader, 1) != 0) { + return -1; + } + *out = reader->data[reader->pos++]; + return 0; +} + +static int dv_reader_read_be(JSDvReader *reader, size_t width, uint64_t *out) { + if (dv_reader_need(reader, width) != 0) { + return -1; + } + uint64_t value = 0; + for (size_t i = 0; i < width; i++) { + value = (value << 8) | reader->data[reader->pos + i]; + } + reader->pos += width; + *out = value; + return 0; +} + +static int dv_read_length(JSDvReader *reader, uint8_t additional, uint64_t *out) { + if (additional <= 23) { + *out = additional; + return 0; + } + + uint64_t value = 0; + switch (additional) { + case 24: + if (dv_reader_read_be(reader, 1, &value) != 0) { + return -1; + } + if (value < 24) { + return dv_throw(reader->ctx, "length not using shortest encoding"); + } + break; + case 25: + if (dv_reader_read_be(reader, 2, &value) != 0) { + return -1; + } + if (value <= 0xff) { + return dv_throw(reader->ctx, "length not using shortest encoding"); + } + break; + case 26: + if (dv_reader_read_be(reader, 4, &value) != 0) { + return -1; + } + if (value <= 0xffff) { + return dv_throw(reader->ctx, "length not using shortest encoding"); + } + break; + case 27: + if (dv_reader_read_be(reader, 8, &value) != 0) { + return -1; + } + if (value <= 0xffffffff) { + return dv_throw(reader->ctx, "length not using shortest encoding"); + } + break; + case 31: + return dv_throw(reader->ctx, "indefinite lengths are not allowed"); + default: + return dv_throw(reader->ctx, "unsupported additional info %u", additional); + } + + *out = value; + return 0; +} + +static JSValue dv_decode_value(JSContext *ctx, + JSDvReader *reader, + const JSDvLimits *limits, + uint32_t depth); + +static JSValue dv_decode_text(JSContext *ctx, + JSDvReader *reader, + const JSDvLimits *limits, + uint8_t additional) { + uint64_t length64 = 0; + if (dv_read_length(reader, additional, &length64) != 0) { + return JS_EXCEPTION; + } + + if (length64 > limits->max_string_bytes) { + JS_ThrowTypeError(ctx, + "string exceeds maxStringBytes (%" PRIu64 " > %u)", + length64, + limits->max_string_bytes); + return JS_EXCEPTION; + } + + size_t length = (size_t)length64; + if (dv_reader_need(reader, length) != 0) { + return JS_EXCEPTION; + } + + const uint8_t *bytes = reader->data + reader->pos; + if (dv_validate_utf8(ctx, bytes, length) != 0) { + return JS_EXCEPTION; + } + + JSValue str = JS_NewStringLen(ctx, (const char *)bytes, length); + reader->pos += length; + return str; +} + +static JSValue dv_decode_array(JSContext *ctx, + JSDvReader *reader, + const JSDvLimits *limits, + uint32_t depth, + uint8_t additional) { + uint64_t length64 = 0; + if (dv_read_length(reader, additional, &length64) != 0) { + return JS_EXCEPTION; + } + + if (length64 > limits->max_array_length) { + JS_ThrowTypeError(ctx, + "array length exceeds maxArrayLength (%" PRIu64 " > %u)", + length64, + limits->max_array_length); + return JS_EXCEPTION; + } + + if (depth + 1 > limits->max_depth) { + JS_ThrowTypeError(ctx, "maxDepth %u exceeded", limits->max_depth); + return JS_EXCEPTION; + } + + uint32_t length = (uint32_t)length64; + JSValue arr = JS_NewArray(ctx); + if (JS_IsException(arr)) { + return JS_EXCEPTION; + } + + for (uint32_t i = 0; i < length; i++) { + JSValue element = dv_decode_value(ctx, reader, limits, depth + 1); + if (JS_IsException(element)) { + JS_FreeValue(ctx, arr); + return JS_EXCEPTION; + } + if (JS_SetPropertyUint32(ctx, arr, i, element) < 0) { + JS_FreeValue(ctx, arr); + return JS_EXCEPTION; + } + } + + return arr; +} + +static JSValue dv_decode_map(JSContext *ctx, + JSDvReader *reader, + const JSDvLimits *limits, + uint32_t depth, + uint8_t additional) { + uint64_t length64 = 0; + if (dv_read_length(reader, additional, &length64) != 0) { + return JS_EXCEPTION; + } + + if (length64 > limits->max_map_length) { + JS_ThrowTypeError(ctx, + "map entries exceed maxMapLength (%" PRIu64 " > %u)", + length64, + limits->max_map_length); + return JS_EXCEPTION; + } + + if (depth + 1 > limits->max_depth) { + JS_ThrowTypeError(ctx, "maxDepth %u exceeded", limits->max_depth); + return JS_EXCEPTION; + } + + uint32_t length = (uint32_t)length64; + JSValue obj = JS_NewObjectProto(ctx, JS_NULL); + if (JS_IsException(obj)) { + return JS_EXCEPTION; + } + + uint8_t *prev_key = NULL; + size_t prev_key_len = 0; + + for (uint32_t i = 0; i < length; i++) { + size_t key_start = reader->pos; + uint8_t key_initial; + if (dv_reader_read_u8(reader, &key_initial) != 0) { + JS_FreeValue(ctx, obj); + js_free(ctx, prev_key); + return JS_EXCEPTION; + } + + uint8_t key_major = key_initial >> 5; + if (key_major != DV_CBOR_MAJOR_TEXT) { + JS_FreeValue(ctx, obj); + js_free(ctx, prev_key); + JS_ThrowTypeError(ctx, "map keys must be text strings"); + return JS_EXCEPTION; + } + + JSValue key_val = dv_decode_text(ctx, reader, limits, key_initial & 0x1f); + if (JS_IsException(key_val)) { + JS_FreeValue(ctx, obj); + js_free(ctx, prev_key); + return JS_EXCEPTION; + } + + size_t key_end = reader->pos; + size_t key_len = key_end - key_start; + const uint8_t *key_bytes = reader->data + key_start; + + if (prev_key) { + int cmp = dv_compare_encoded_keys(&(JSDvKeyEntry){.encoded_key = prev_key, .encoded_key_len = prev_key_len}, + &(JSDvKeyEntry){.encoded_key = (uint8_t *)key_bytes, .encoded_key_len = key_len}); + if (cmp == 0) { + JS_ThrowTypeError(ctx, "map contains duplicate key"); + JS_FreeValue(ctx, key_val); + JS_FreeValue(ctx, obj); + js_free(ctx, prev_key); + return JS_EXCEPTION; + } + if (cmp > 0) { + JS_ThrowTypeError(ctx, "map keys are not in canonical order"); + JS_FreeValue(ctx, key_val); + JS_FreeValue(ctx, obj); + js_free(ctx, prev_key); + return JS_EXCEPTION; + } + } + + if (prev_key) { + js_free(ctx, prev_key); + } + prev_key = NULL; + if (key_len > 0) { + prev_key = js_malloc(ctx, key_len); + if (!prev_key) { + JS_FreeValue(ctx, key_val); + JS_FreeValue(ctx, obj); + return JS_EXCEPTION; + } + memcpy(prev_key, key_bytes, key_len); + } + prev_key_len = key_len; + + JSValue decoded = dv_decode_value(ctx, reader, limits, depth + 1); + if (JS_IsException(decoded)) { + JS_FreeValue(ctx, key_val); + JS_FreeValue(ctx, obj); + js_free(ctx, prev_key); + return JS_EXCEPTION; + } + + size_t key_str_len = 0; + const char *key_str = JS_ToCStringLen(ctx, &key_str_len, key_val); + if (!key_str) { + JS_FreeValue(ctx, decoded); + JS_FreeValue(ctx, key_val); + JS_FreeValue(ctx, obj); + js_free(ctx, prev_key); + return JS_EXCEPTION; + } + + JSAtom atom = JS_NewAtomLen(ctx, key_str, key_str_len); + JS_FreeCString(ctx, key_str); + JS_FreeValue(ctx, key_val); + if (atom == JS_ATOM_NULL) { + JS_FreeValue(ctx, decoded); + JS_FreeValue(ctx, obj); + js_free(ctx, prev_key); + return JS_EXCEPTION; + } + + if (JS_DefinePropertyValue(ctx, obj, atom, decoded, JS_PROP_C_W_E) < 0) { + JS_FreeAtom(ctx, atom); + JS_FreeValue(ctx, obj); + js_free(ctx, prev_key); + return JS_EXCEPTION; + } + JS_FreeAtom(ctx, atom); + } + + if (prev_key) { + js_free(ctx, prev_key); + } + return obj; +} + +static JSValue dv_decode_simple_or_float(JSContext *ctx, + JSDvReader *reader, + const JSDvLimits *limits, + uint8_t additional) { + (void)limits; + switch (additional) { + case 20: + return JS_FALSE; + case 21: + return JS_TRUE; + case 22: + return JS_NULL; + case 27: { + uint64_t bits = 0; + if (dv_reader_read_be(reader, 8, &bits) != 0) { + return JS_EXCEPTION; + } + union { + uint64_t u; + double d; + } u; + u.u = bits; + if (!isfinite(u.d)) { + JS_ThrowTypeError(ctx, "DV numbers must be finite"); + return JS_EXCEPTION; + } + double int_part; + if (modf(u.d, &int_part) == 0.0) { + JS_ThrowTypeError(ctx, "integers must use CBOR integer encoding"); + return JS_EXCEPTION; + } + if (u.d == 0 && signbit(u.d)) { + u.d = 0; + } + return JS_NewFloat64(ctx, u.d); + } + case 24: + case 25: + case 26: + JS_ThrowTypeError(ctx, "only float64 is allowed"); + return JS_EXCEPTION; + case 31: + JS_ThrowTypeError(ctx, "indefinite lengths are not allowed"); + return JS_EXCEPTION; + default: + JS_ThrowTypeError(ctx, "unsupported simple value %u", additional); + return JS_EXCEPTION; + } +} + +static JSValue dv_decode_value(JSContext *ctx, + JSDvReader *reader, + const JSDvLimits *limits, + uint32_t depth) { + uint8_t initial = 0; + if (dv_reader_read_u8(reader, &initial) != 0) { + return JS_EXCEPTION; + } + + uint8_t major = initial >> 5; + uint8_t additional = initial & 0x1f; + + switch (major) { + case DV_CBOR_MAJOR_UINT: { + uint64_t value = 0; + if (dv_read_length(reader, additional, &value) != 0) { + return JS_EXCEPTION; + } + if (value > (uint64_t)dv_max_safe_int) { + JS_ThrowTypeError(ctx, "integer is outside safe range (%" PRIu64 " > %" PRIu64 ")", + value, + (uint64_t)dv_max_safe_int); + return JS_EXCEPTION; + } + return JS_NewInt64(ctx, (int64_t)value); + } + case DV_CBOR_MAJOR_NINT: { + uint64_t value = 0; + if (dv_read_length(reader, additional, &value) != 0) { + return JS_EXCEPTION; + } + if (value >= (uint64_t)dv_max_safe_int) { + JS_ThrowTypeError(ctx, "integer is outside safe range (-1 - %" PRIu64 " < %" PRId64 ")", + value, + dv_min_safe_int); + return JS_EXCEPTION; + } + int64_t neg = -1 - (int64_t)value; + return JS_NewInt64(ctx, neg); + } + case DV_CBOR_MAJOR_TEXT: + return dv_decode_text(ctx, reader, limits, additional); + case DV_CBOR_MAJOR_ARRAY: + return dv_decode_array(ctx, reader, limits, depth, additional); + case DV_CBOR_MAJOR_MAP: + return dv_decode_map(ctx, reader, limits, depth, additional); + case DV_CBOR_MAJOR_SIMPLE: + return dv_decode_simple_or_float(ctx, reader, limits, additional); + default: + JS_ThrowTypeError(ctx, "unsupported CBOR major type %u", major); + return JS_EXCEPTION; + } +} + +JSValue JS_DecodeDV(JSContext *ctx, + const uint8_t *data, + size_t length, + const JSDvLimits *maybe_limits) { + const JSDvLimits *limits = dv_limits_or_default(maybe_limits); + if (length > limits->max_encoded_bytes) { + JS_ThrowTypeError(ctx, + "encoded DV exceeds maxEncodedBytes (%zu > %u)", + length, + limits->max_encoded_bytes); + return JS_EXCEPTION; + } + + JSDvReader reader = { + .data = data, + .size = length, + .pos = 0, + .ctx = ctx, + }; + + JSValue result = dv_decode_value(ctx, &reader, limits, 0); + if (JS_IsException(result)) { + return result; + } + + if (reader.pos != reader.size) { + JS_FreeValue(ctx, result); + JS_ThrowTypeError(ctx, "unexpected trailing bytes after DV value"); + return JS_EXCEPTION; + } + + return result; +} + +void JS_FreeDVBuffer(JSContext *ctx, JSDvBuffer *buffer) { + if (!buffer || !buffer->data) { + return; + } + js_free(ctx, buffer->data); + buffer->data = NULL; + buffer->length = 0; +} + +const JSDvLimits JS_DV_LIMIT_DEFAULTS = { + .max_depth = 64, + .max_encoded_bytes = 1048576, + .max_string_bytes = 262144, + .max_array_length = 65535, + .max_map_length = 65535, +}; diff --git a/quickjs.h b/quickjs.h index 6d362b505..c850ba7e2 100644 --- a/quickjs.h +++ b/quickjs.h @@ -411,6 +411,26 @@ int JS_EnableGasTrace(JSContext *ctx, int enabled); int JS_ResetGasTrace(JSContext *ctx); int JS_ReadGasTrace(JSContext *ctx, JSGasTrace *out_trace); int JS_UseGas(JSContext *ctx, uint64_t amount); + +typedef struct JSDvLimits { + uint32_t max_depth; + uint32_t max_encoded_bytes; + uint32_t max_string_bytes; + uint32_t max_array_length; + uint32_t max_map_length; +} JSDvLimits; + +typedef struct JSDvBuffer { + uint8_t *data; + size_t length; +} JSDvBuffer; + +extern const JSDvLimits JS_DV_LIMIT_DEFAULTS; + +int JS_EncodeDV(JSContext *ctx, JSValueConst value, const JSDvLimits *limits, JSDvBuffer *out_buffer); +JSValue JS_DecodeDV(JSContext *ctx, const uint8_t *data, size_t length, const JSDvLimits *limits); +void JS_FreeDVBuffer(JSContext *ctx, JSDvBuffer *buffer); + JSRuntime *JS_GetRuntime(JSContext *ctx); void JS_SetClassProto(JSContext *ctx, JSClassID class_id, JSValue obj); JSValue JS_GetClassProto(JSContext *ctx, JSClassID class_id); From 9cfaffe5ec38765dc01ec118f9895128a90109e4 Mon Sep 17 00:00:00 2001 From: Mateusz Jonak Date: Tue, 16 Dec 2025 11:12:25 +0100 Subject: [PATCH 14/19] feat(quickjs): add deterministic context init with ABI manifest hash check Introduce JS_InitDeterministicContext() and JSDeterministicInitOptions to initialize a deterministic JSContext from an ABI manifest + optional context blob and gas limit. Validate manifest size and required hash, compute/compare SHA-256, throw a ManifestError (ABI_MANIFEST_HASH_MISMATCH) on mismatch, and store owned copies on the context (freed in JS_FreeContext). Add internal SHA-256 helpers and link quickjs-dv/quickjs-sha256 into the QuickJS lib build. --- Makefile | 2 +- quickjs-internal.h | 11 +++ quickjs-sha256.c | 163 +++++++++++++++++++++++++++++++++++++++++++++ quickjs.c | 158 +++++++++++++++++++++++++++++++++++++++++++ quickjs.h | 12 ++++ 5 files changed, 345 insertions(+), 1 deletion(-) create mode 100644 quickjs-internal.h create mode 100644 quickjs-sha256.c diff --git a/Makefile b/Makefile index dcbbf7e5e..47e5ac16e 100644 --- a/Makefile +++ b/Makefile @@ -243,7 +243,7 @@ endif all: $(OBJDIR) $(OBJDIR)/quickjs.check.o $(OBJDIR)/qjs.check.o $(PROGS) -QJS_LIB_OBJS=$(OBJDIR)/quickjs.o $(OBJDIR)/dtoa.o $(OBJDIR)/libregexp.o $(OBJDIR)/libunicode.o $(OBJDIR)/cutils.o $(OBJDIR)/quickjs-libc.o +QJS_LIB_OBJS=$(OBJDIR)/quickjs.o $(OBJDIR)/quickjs-dv.o $(OBJDIR)/quickjs-sha256.o $(OBJDIR)/dtoa.o $(OBJDIR)/libregexp.o $(OBJDIR)/libunicode.o $(OBJDIR)/cutils.o $(OBJDIR)/quickjs-libc.o QJS_OBJS=$(OBJDIR)/qjs.o $(OBJDIR)/repl.o $(QJS_LIB_OBJS) diff --git a/quickjs-internal.h b/quickjs-internal.h new file mode 100644 index 000000000..942ca0801 --- /dev/null +++ b/quickjs-internal.h @@ -0,0 +1,11 @@ +#ifndef QUICKJS_INTERNAL_H +#define QUICKJS_INTERNAL_H + +#include +#include + +/* Internal helpers not exposed in the public QuickJS API surface. */ +void js_sha256(const uint8_t *data, size_t len, uint8_t out_hash[32]); +void js_sha256_to_hex(const uint8_t hash[32], char out_hex[65]); + +#endif /* QUICKJS_INTERNAL_H */ diff --git a/quickjs-sha256.c b/quickjs-sha256.c new file mode 100644 index 000000000..3cb4896b7 --- /dev/null +++ b/quickjs-sha256.c @@ -0,0 +1,163 @@ +#include +#include +#include + +typedef struct { + uint8_t data[64]; + uint32_t datalen; + uint64_t bitlen; + uint32_t state[8]; +} JSSHA256Context; + +static const uint32_t js_sha256_k[64] = { + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, + 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, + 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, + 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, + 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, + 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, + 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, + 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, + 0xc67178f2}; + +#define JS_SHA256_ROTR(x, n) (((x) >> (n)) | ((x) << (32 - (n)))) +#define JS_SHA256_CH(x, y, z) (((x) & (y)) ^ (~(x) & (z))) +#define JS_SHA256_MAJ(x, y, z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z))) +#define JS_SHA256_EP0(x) (JS_SHA256_ROTR(x, 2) ^ JS_SHA256_ROTR(x, 13) ^ JS_SHA256_ROTR(x, 22)) +#define JS_SHA256_EP1(x) (JS_SHA256_ROTR(x, 6) ^ JS_SHA256_ROTR(x, 11) ^ JS_SHA256_ROTR(x, 25)) +#define JS_SHA256_SIG0(x) (JS_SHA256_ROTR(x, 7) ^ JS_SHA256_ROTR(x, 18) ^ ((x) >> 3)) +#define JS_SHA256_SIG1(x) (JS_SHA256_ROTR(x, 17) ^ JS_SHA256_ROTR(x, 19) ^ ((x) >> 10)) + +static void js_sha256_transform(JSSHA256Context *ctx, const uint8_t data[64]) +{ + uint32_t m[64]; + uint32_t a, b, c, d, e, f, g, h; + size_t i; + + for(i = 0; i < 16; i++) { + m[i] = ((uint32_t)data[i * 4] << 24) | ((uint32_t)data[i * 4 + 1] << 16) | + ((uint32_t)data[i * 4 + 2] << 8) | ((uint32_t)data[i * 4 + 3]); + } + + for(; i < 64; i++) { + m[i] = JS_SHA256_SIG1(m[i - 2]) + m[i - 7] + JS_SHA256_SIG0(m[i - 15]) + m[i - 16]; + } + + a = ctx->state[0]; + b = ctx->state[1]; + c = ctx->state[2]; + d = ctx->state[3]; + e = ctx->state[4]; + f = ctx->state[5]; + g = ctx->state[6]; + h = ctx->state[7]; + + for(i = 0; i < 64; i++) { + uint32_t t1 = h + JS_SHA256_EP1(e) + JS_SHA256_CH(e, f, g) + js_sha256_k[i] + m[i]; + uint32_t t2 = JS_SHA256_EP0(a) + JS_SHA256_MAJ(a, b, c); + h = g; + g = f; + f = e; + e = d + t1; + d = c; + c = b; + b = a; + a = t1 + t2; + } + + ctx->state[0] += a; + ctx->state[1] += b; + ctx->state[2] += c; + ctx->state[3] += d; + ctx->state[4] += e; + ctx->state[5] += f; + ctx->state[6] += g; + ctx->state[7] += h; +} + +static void js_sha256_init(JSSHA256Context *ctx) +{ + ctx->datalen = 0; + ctx->bitlen = 0; + ctx->state[0] = 0x6a09e667; + ctx->state[1] = 0xbb67ae85; + ctx->state[2] = 0x3c6ef372; + ctx->state[3] = 0xa54ff53a; + ctx->state[4] = 0x510e527f; + ctx->state[5] = 0x9b05688c; + ctx->state[6] = 0x1f83d9ab; + ctx->state[7] = 0x5be0cd19; +} + +static void js_sha256_update(JSSHA256Context *ctx, const uint8_t *data, size_t len) +{ + for(size_t i = 0; i < len; i++) { + ctx->data[ctx->datalen] = data[i]; + ctx->datalen++; + if(ctx->datalen == 64) { + js_sha256_transform(ctx, ctx->data); + ctx->bitlen += 512; + ctx->datalen = 0; + } + } +} + +static void js_sha256_final(JSSHA256Context *ctx, uint8_t hash[32]) +{ + uint32_t i = ctx->datalen; + + if(ctx->datalen < 56) { + ctx->data[i++] = 0x80; + while(i < 56) { + ctx->data[i++] = 0x00; + } + } else { + ctx->data[i++] = 0x80; + while(i < 64) { + ctx->data[i++] = 0x00; + } + js_sha256_transform(ctx, ctx->data); + memset(ctx->data, 0, 56); + } + + ctx->bitlen += (uint64_t)ctx->datalen * 8; + ctx->data[63] = (uint8_t)(ctx->bitlen); + ctx->data[62] = (uint8_t)(ctx->bitlen >> 8); + ctx->data[61] = (uint8_t)(ctx->bitlen >> 16); + ctx->data[60] = (uint8_t)(ctx->bitlen >> 24); + ctx->data[59] = (uint8_t)(ctx->bitlen >> 32); + ctx->data[58] = (uint8_t)(ctx->bitlen >> 40); + ctx->data[57] = (uint8_t)(ctx->bitlen >> 48); + ctx->data[56] = (uint8_t)(ctx->bitlen >> 56); + js_sha256_transform(ctx, ctx->data); + + for(i = 0; i < 4; i++) { + hash[i] = (uint8_t)((ctx->state[0] >> (24 - i * 8)) & 0xff); + hash[i + 4] = (uint8_t)((ctx->state[1] >> (24 - i * 8)) & 0xff); + hash[i + 8] = (uint8_t)((ctx->state[2] >> (24 - i * 8)) & 0xff); + hash[i + 12] = (uint8_t)((ctx->state[3] >> (24 - i * 8)) & 0xff); + hash[i + 16] = (uint8_t)((ctx->state[4] >> (24 - i * 8)) & 0xff); + hash[i + 20] = (uint8_t)((ctx->state[5] >> (24 - i * 8)) & 0xff); + hash[i + 24] = (uint8_t)((ctx->state[6] >> (24 - i * 8)) & 0xff); + hash[i + 28] = (uint8_t)((ctx->state[7] >> (24 - i * 8)) & 0xff); + } +} + +void js_sha256(const uint8_t *data, size_t len, uint8_t out_hash[32]) +{ + JSSHA256Context ctx; + js_sha256_init(&ctx); + js_sha256_update(&ctx, data, len); + js_sha256_final(&ctx, out_hash); +} + +void js_sha256_to_hex(const uint8_t hash[32], char out_hex[65]) +{ + static const char hex_alphabet[] = "0123456789abcdef"; + for(size_t i = 0; i < 32; i++) { + out_hex[i * 2] = hex_alphabet[hash[i] >> 4]; + out_hex[i * 2 + 1] = hex_alphabet[hash[i] & 0x0f]; + } + out_hex[64] = '\0'; +} diff --git a/quickjs.c b/quickjs.c index ac1493263..cf6a56207 100644 --- a/quickjs.c +++ b/quickjs.c @@ -42,6 +42,7 @@ #include "cutils.h" #include "list.h" +#include "quickjs-internal.h" #include "quickjs.h" #include "libregexp.h" #include "libunicode.h" @@ -500,6 +501,13 @@ struct JSContext { uint64_t gas_remaining; uint32_t gas_version; + uint8_t *abi_manifest_bytes; + size_t abi_manifest_size; + uint8_t abi_manifest_hash[32]; + char abi_manifest_hash_hex[65]; + uint8_t *deterministic_context_blob; + size_t deterministic_context_blob_size; + JSGasTraceData *gas_trace; BOOL deterministic_mode; }; @@ -2941,6 +2949,151 @@ int JS_NewDeterministicRuntime(JSRuntime **out_rt, JSContext **out_ctx) return 0; } +static JSValue JS_ThrowManifestError(JSContext *ctx, const char *code, const char *message) +{ + JSValue obj, name, msg, code_val; + + obj = JS_NewError(ctx); + if (JS_IsException(obj)) + return JS_EXCEPTION; + + name = JS_NewString(ctx, "ManifestError"); + msg = JS_NewString(ctx, message); + code_val = JS_NewString(ctx, code); + if (JS_IsException(name) || JS_IsException(msg) || JS_IsException(code_val)) { + if (!JS_IsException(name)) + JS_FreeValue(ctx, name); + if (!JS_IsException(msg)) + JS_FreeValue(ctx, msg); + if (!JS_IsException(code_val)) + JS_FreeValue(ctx, code_val); + JS_FreeValue(ctx, obj); + return JS_EXCEPTION; + } + + JS_DefinePropertyValue(ctx, obj, JS_ATOM_name, name, + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + JS_DefinePropertyValue(ctx, obj, JS_ATOM_message, msg, + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + JS_DefinePropertyValueStr(ctx, obj, "code", code_val, + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + JS_Throw(ctx, obj); + return JS_EXCEPTION; +} + +static int js_hex_nibble(int c) +{ + if (c >= '0' && c <= '9') + return c - '0'; + if (c >= 'a' && c <= 'f') + return c - 'a' + 10; + return -1; +} + +static int js_parse_hash_hex(JSContext *ctx, const char *hex, uint8_t *out, size_t out_size) +{ + size_t hex_len, i; + + if (!hex || !out) + return -1; + + hex_len = strlen(hex); + if (hex_len != out_size * 2) { + JS_ThrowTypeError(ctx, "abi manifest hash must be 64 lowercase hex characters"); + return -1; + } + + for(i = 0; i < out_size; i++) { + int high = js_hex_nibble(hex[i * 2]); + int low = js_hex_nibble(hex[i * 2 + 1]); + if (high < 0 || low < 0) { + JS_ThrowTypeError(ctx, "abi manifest hash must be 64 lowercase hex characters"); + return -1; + } + out[i] = (uint8_t)((high << 4) | low); + } + + return 0; +} + +int JS_InitDeterministicContext(JSContext *ctx, const JSDeterministicInitOptions *options) +{ + uint8_t computed_hash[32]; + uint8_t expected_hash[32]; + uint8_t *manifest_copy = NULL; + uint8_t *context_copy = NULL; + + if (!ctx || !options) + return -1; + + if (ctx->abi_manifest_bytes) { + JS_ThrowTypeError(ctx, "abi manifest is already initialized"); + return -1; + } + + if (!options->manifest_bytes || options->manifest_size == 0) { + JS_ThrowTypeError(ctx, "abi manifest is required"); + return -1; + } + + if (options->manifest_size > JS_DETERMINISTIC_MAX_MANIFEST_BYTES) { + JS_ThrowTypeError(ctx, "abi manifest exceeds maximum size"); + return -1; + } + + if (!options->manifest_hash_hex) { + JS_ThrowTypeError(ctx, "abi manifest hash is required"); + return -1; + } + + if (js_parse_hash_hex(ctx, options->manifest_hash_hex, expected_hash, sizeof(expected_hash)) != 0) + return -1; + + if (options->context_blob_size > 0 && !options->context_blob) { + JS_ThrowTypeError(ctx, "context blob is required when context_blob_size is set"); + return -1; + } + + js_sha256(options->manifest_bytes, options->manifest_size, computed_hash); + if (memcmp(computed_hash, expected_hash, sizeof(expected_hash)) != 0) { + JS_ThrowManifestError(ctx, "ABI_MANIFEST_HASH_MISMATCH", "abi manifest hash mismatch"); + return -1; + } + + manifest_copy = js_malloc_rt(ctx->rt, options->manifest_size); + if (!manifest_copy) { + JS_ThrowOutOfMemory(ctx); + return -1; + } + memcpy(manifest_copy, options->manifest_bytes, options->manifest_size); + + if (options->context_blob && options->context_blob_size > 0) { + if (options->context_blob_size > JS_DETERMINISTIC_MAX_CONTEXT_BLOB_BYTES) { + js_free_rt(ctx->rt, manifest_copy); + JS_ThrowTypeError(ctx, "context blob exceeds maximum size"); + return -1; + } + + context_copy = js_malloc_rt(ctx->rt, options->context_blob_size); + if (!context_copy) { + js_free_rt(ctx->rt, manifest_copy); + JS_ThrowOutOfMemory(ctx); + return -1; + } + memcpy(context_copy, options->context_blob, options->context_blob_size); + } + + memcpy(ctx->abi_manifest_hash, computed_hash, sizeof(computed_hash)); + js_sha256_to_hex(computed_hash, ctx->abi_manifest_hash_hex); + ctx->abi_manifest_bytes = manifest_copy; + ctx->abi_manifest_size = options->manifest_size; + ctx->deterministic_context_blob = context_copy; + ctx->deterministic_context_blob_size = context_copy ? options->context_blob_size : 0; + JS_SetGasLimit(ctx, options->gas_limit); + + return 0; +} + void *JS_GetContextOpaque(JSContext *ctx) { return ctx->user_opaque; @@ -3262,6 +3415,11 @@ void JS_FreeContext(JSContext *ctx) js_free_shape_null(ctx->rt, ctx->regexp_shape); js_free_shape_null(ctx->rt, ctx->regexp_result_shape); + if (ctx->abi_manifest_bytes) + js_free_rt(ctx->rt, ctx->abi_manifest_bytes); + if (ctx->deterministic_context_blob) + js_free_rt(ctx->rt, ctx->deterministic_context_blob); + list_del(&ctx->link); remove_gc_object(&ctx->header); if (ctx->gas_trace) diff --git a/quickjs.h b/quickjs.h index c850ba7e2..6fbafed20 100644 --- a/quickjs.h +++ b/quickjs.h @@ -343,6 +343,9 @@ static inline JSValue __JS_NewShortBigInt(JSContext *ctx, int64_t d) promise. Only allowed with JS_EVAL_TYPE_GLOBAL */ #define JS_EVAL_FLAG_ASYNC (1 << 7) +#define JS_DETERMINISTIC_MAX_MANIFEST_BYTES 1048576 +#define JS_DETERMINISTIC_MAX_CONTEXT_BLOB_BYTES 1048576 + typedef JSValue JSCFunction(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv); typedef JSValue JSCFunctionMagic(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic); typedef JSValue JSCFunctionData(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic, JSValue *func_data); @@ -388,6 +391,15 @@ JS_BOOL JS_IsLiveObject(JSRuntime *rt, JSValueConst obj); JSContext *JS_NewContext(JSRuntime *rt); int JS_NewDeterministicRuntime(JSRuntime **out_rt, JSContext **out_ctx); +typedef struct JSDeterministicInitOptions { + const uint8_t *manifest_bytes; + size_t manifest_size; + const char *manifest_hash_hex; + const uint8_t *context_blob; + size_t context_blob_size; + uint64_t gas_limit; +} JSDeterministicInitOptions; +int JS_InitDeterministicContext(JSContext *ctx, const JSDeterministicInitOptions *options); void JS_FreeContext(JSContext *s); JSContext *JS_DupContext(JSContext *ctx); void *JS_GetContextOpaque(JSContext *ctx); From cd0258181535fd74d5eb10adc70f6ecce31317a9 Mon Sep 17 00:00:00 2001 From: Mateusz Jonak Date: Tue, 16 Dec 2025 14:35:44 +0100 Subject: [PATCH 15/19] feat(quickjs): add deterministic host_call dispatcher API (wasm import + reentrancy guard) - add JS_SetHostCallDispatcher() and JS_HostCall() public API, including JSHostCallFunc/JSHostCallResult and JS_HOST_CALL_TRANSPORT_ERROR - implement host_call plumbing in deterministic runtime with per-context scratch response buffer + bounds/limit checks - on Emscripten, default dispatcher calls imported host.host_call; otherwise dispatcher is unset by default - prevent reentrant host calls via rt->in_host_call and free host_call response buffer on context teardown --- quickjs.c | 165 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ quickjs.h | 25 +++++++++ 2 files changed, 190 insertions(+) diff --git a/quickjs.c b/quickjs.c index cf6a56207..e54a2fc4c 100644 --- a/quickjs.c +++ b/quickjs.c @@ -281,6 +281,8 @@ struct JSRuntime { BOOL in_out_of_memory : 8; /* true if inside out-of-gas handling to avoid recursion */ BOOL in_out_of_gas : 8; + /* true if inside a host_call to prevent reentrancy */ + BOOL in_host_call : 8; struct JSStackFrame *current_stack_frame; @@ -289,6 +291,8 @@ struct JSRuntime { JSHostPromiseRejectionTracker *host_promise_rejection_tracker; void *host_promise_rejection_tracker_opaque; + JSHostCallFunc *host_call_func; + void *host_call_opaque; struct list_head job_list; /* list of JSJobEntry.link */ @@ -508,6 +512,9 @@ struct JSContext { uint8_t *deterministic_context_blob; size_t deterministic_context_blob_size; + uint8_t *host_call_resp_buf; + uint32_t host_call_resp_capacity; + JSGasTraceData *gas_trace; BOOL deterministic_mode; }; @@ -1883,6 +1890,16 @@ void JS_SetRuntimeOpaque(JSRuntime *rt, void *opaque) rt->user_opaque = opaque; } +int JS_SetHostCallDispatcher(JSRuntime *rt, JSHostCallFunc *func, void *opaque) +{ + if (!rt) + return -1; + + rt->host_call_func = func; + rt->host_call_opaque = opaque; + return 0; +} + /* default memory allocation functions with memory limitation */ static size_t js_def_malloc_usable_size(const void *ptr) { @@ -2912,6 +2929,25 @@ static int js_deterministic_init_context(JSContext *ctx) return 0; } +#ifdef __EMSCRIPTEN__ +__attribute__((import_module("host"), import_name("host_call"))) +uint32_t js_wasm_import_host_call(uint32_t fn_id, uint32_t req_ptr, uint32_t req_len, + uint32_t resp_ptr, uint32_t resp_capacity); + +static uint32_t js_wasm_host_call(JSContext *ctx, uint32_t fn_id, const uint8_t *req_ptr, + uint32_t req_len, uint8_t *resp_ptr, + uint32_t resp_capacity, void *opaque) +{ + (void)ctx; + (void)opaque; + return js_wasm_import_host_call(fn_id, + (uint32_t)(uintptr_t)req_ptr, + req_len, + (uint32_t)(uintptr_t)resp_ptr, + resp_capacity); +} +#endif + int JS_NewDeterministicRuntime(JSRuntime **out_rt, JSContext **out_ctx) { JSRuntime *rt; @@ -2930,6 +2966,13 @@ int JS_NewDeterministicRuntime(JSRuntime **out_rt, JSContext **out_ctx) rt->deterministic_mode = TRUE; rt->det_gc_pending = FALSE; rt->det_gc_alloc_bytes = 0; +#ifdef __EMSCRIPTEN__ + rt->host_call_func = js_wasm_host_call; +#else + rt->host_call_func = NULL; +#endif + rt->host_call_opaque = NULL; + rt->in_host_call = FALSE; JS_SetGCThreshold(rt, (size_t)-1); ctx = JS_NewContextRaw(rt); @@ -3094,6 +3137,126 @@ int JS_InitDeterministicContext(JSContext *ctx, const JSDeterministicInitOptions return 0; } +static int js_reserve_host_response_buffer(JSContext *ctx, uint32_t capacity) +{ + uint8_t *new_buf; + + if (ctx->host_call_resp_capacity >= capacity) + return 0; + + new_buf = js_realloc_rt(ctx->rt, ctx->host_call_resp_buf, capacity); + if (!new_buf) { + JS_ThrowOutOfMemory(ctx); + return -1; + } + + ctx->host_call_resp_buf = new_buf; + ctx->host_call_resp_capacity = capacity; + return 0; +} + +int JS_HostCall(JSContext *ctx, + uint32_t fn_id, + const uint8_t *req_bytes, + size_t req_len, + uint32_t max_request_bytes, + uint32_t max_response_bytes, + JSHostCallResult *out_result) +{ + JSRuntime *rt; + uint32_t resp_len, resp_capacity, req_len32; + uint32_t dv_limit; + const uint8_t *req_ptr; + + if (!ctx || !out_result) + return -1; + + out_result->data = NULL; + out_result->length = 0; + + rt = ctx->rt; + dv_limit = JS_DV_LIMIT_DEFAULTS.max_encoded_bytes; + + if (!rt->host_call_func) { + JS_ThrowTypeError(ctx, "host_call dispatcher is not configured"); + return -1; + } + + if (fn_id == 0) { + JS_ThrowTypeError(ctx, "host_call fn_id must be >= 1"); + return -1; + } + + if (max_request_bytes == 0) { + JS_ThrowTypeError(ctx, "host_call max_request_bytes must be > 0"); + return -1; + } + + if (max_request_bytes > dv_limit) { + JS_ThrowTypeError(ctx, "host_call max_request_bytes exceeds DV limit"); + return -1; + } + + if (max_response_bytes == 0) { + JS_ThrowTypeError(ctx, "host_call max_response_bytes must be > 0"); + return -1; + } + + if (max_response_bytes > dv_limit) { + JS_ThrowTypeError(ctx, "host_call max_response_bytes exceeds DV limit"); + return -1; + } + + if (req_len > (size_t)max_request_bytes) { + JS_ThrowTypeError(ctx, "host_call request exceeds max_request_bytes"); + return -1; + } + + if (req_len > (size_t)dv_limit) { + JS_ThrowTypeError(ctx, "host_call request exceeds DV limit"); + return -1; + } + + if (req_len > UINT32_MAX) { + JS_ThrowTypeError(ctx, "host_call request length overflow"); + return -1; + } + + if (req_len > 0 && !req_bytes) { + JS_ThrowTypeError(ctx, "host_call request pointer is null"); + return -1; + } + + if (rt->in_host_call) { + JS_ThrowTypeError(ctx, "host_call is already in progress"); + return -1; + } + + resp_capacity = max_response_bytes; + if (js_reserve_host_response_buffer(ctx, resp_capacity)) + return -1; + + rt->in_host_call = TRUE; + req_len32 = (uint32_t)req_len; + req_ptr = req_bytes ? req_bytes : NULL; + resp_len = rt->host_call_func(ctx, fn_id, req_ptr, req_len32, + ctx->host_call_resp_buf, resp_capacity, + rt->host_call_opaque); + rt->in_host_call = FALSE; + + if (JS_HasException(ctx)) + return -1; + + if (resp_len == JS_HOST_CALL_TRANSPORT_ERROR || resp_len > resp_capacity) { + JS_ThrowTypeError(ctx, "host_call transport failed"); + return -1; + } + + out_result->data = ctx->host_call_resp_buf; + out_result->length = resp_len; + return 0; +} + void *JS_GetContextOpaque(JSContext *ctx) { return ctx->user_opaque; @@ -3419,6 +3582,8 @@ void JS_FreeContext(JSContext *ctx) js_free_rt(ctx->rt, ctx->abi_manifest_bytes); if (ctx->deterministic_context_blob) js_free_rt(ctx->rt, ctx->deterministic_context_blob); + if (ctx->host_call_resp_buf) + js_free_rt(ctx->rt, ctx->host_call_resp_buf); list_del(&ctx->link); remove_gc_object(&ctx->header); diff --git a/quickjs.h b/quickjs.h index 6fbafed20..5c817d80a 100644 --- a/quickjs.h +++ b/quickjs.h @@ -424,6 +424,31 @@ int JS_ResetGasTrace(JSContext *ctx); int JS_ReadGasTrace(JSContext *ctx, JSGasTrace *out_trace); int JS_UseGas(JSContext *ctx, uint64_t amount); +#define JS_HOST_CALL_TRANSPORT_ERROR UINT32_C(0xffffffff) + +typedef struct JSHostCallResult { + uint8_t *data; + uint32_t length; +} JSHostCallResult; +/* data points to an internal scratch buffer owned by the context; valid until the next JS_HostCall or context free */ + +typedef uint32_t JSHostCallFunc(JSContext *ctx, + uint32_t fn_id, + const uint8_t *req_ptr, + uint32_t req_len, + uint8_t *resp_ptr, + uint32_t resp_capacity, + void *opaque); + +int JS_SetHostCallDispatcher(JSRuntime *rt, JSHostCallFunc *func, void *opaque); +int JS_HostCall(JSContext *ctx, + uint32_t fn_id, + const uint8_t *req_bytes, + size_t req_len, + uint32_t max_request_bytes, + uint32_t max_response_bytes, + JSHostCallResult *out_result); + typedef struct JSDvLimits { uint32_t max_depth; uint32_t max_encoded_bytes; From 17b19102ad73b0d785f2855d7bf0799fef969d9c Mon Sep 17 00:00:00 2001 From: Mateusz Jonak Date: Tue, 16 Dec 2025 19:30:30 +0100 Subject: [PATCH 16/19] feat(quickjs-host): add HostError + host response envelope helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `quickjs-host.{c,h}` implementing `HostError` construction and host response envelope parsing (T-039), including DV decode, strict envelope validation, units bounds checks, and whitelisted error code→tag mapping. - Wire `quickjs-host.o` into `QJS_LIB_OBJS` - Use `JS_ThrowHostTransportError()` instead of a generic `TypeError` on transport failure Refs: T-039 --- Makefile | 2 +- quickjs-host.c | 401 +++++++++++++++++++++++++++++++++++++++++++++++++ quickjs-host.h | 36 +++++ quickjs.c | 3 +- 4 files changed, 440 insertions(+), 2 deletions(-) create mode 100644 quickjs-host.c create mode 100644 quickjs-host.h diff --git a/Makefile b/Makefile index 47e5ac16e..f6092cd22 100644 --- a/Makefile +++ b/Makefile @@ -243,7 +243,7 @@ endif all: $(OBJDIR) $(OBJDIR)/quickjs.check.o $(OBJDIR)/qjs.check.o $(PROGS) -QJS_LIB_OBJS=$(OBJDIR)/quickjs.o $(OBJDIR)/quickjs-dv.o $(OBJDIR)/quickjs-sha256.o $(OBJDIR)/dtoa.o $(OBJDIR)/libregexp.o $(OBJDIR)/libunicode.o $(OBJDIR)/cutils.o $(OBJDIR)/quickjs-libc.o +QJS_LIB_OBJS=$(OBJDIR)/quickjs.o $(OBJDIR)/quickjs-host.o $(OBJDIR)/quickjs-dv.o $(OBJDIR)/quickjs-sha256.o $(OBJDIR)/dtoa.o $(OBJDIR)/libregexp.o $(OBJDIR)/libunicode.o $(OBJDIR)/cutils.o $(OBJDIR)/quickjs-libc.o QJS_OBJS=$(OBJDIR)/qjs.o $(OBJDIR)/repl.o $(QJS_LIB_OBJS) diff --git a/quickjs-host.c b/quickjs-host.c new file mode 100644 index 000000000..d2295fe94 --- /dev/null +++ b/quickjs-host.c @@ -0,0 +1,401 @@ +#include "cutils.h" +#include "quickjs-host.h" +#include "quickjs-internal.h" +#include +#include + +#define JS_HOST_ERROR_CODE_TRANSPORT "HOST_TRANSPORT" +#define JS_HOST_ERROR_TAG_TRANSPORT "host/transport" +#define JS_HOST_ERROR_CODE_ENVELOPE_INVALID "HOST_ENVELOPE_INVALID" +#define JS_HOST_ERROR_TAG_ENVELOPE_INVALID "host/envelope_invalid" + +static JSValue js_throw_host_error_str(JSContext *ctx, const char *code, const char *tag, JSValueConst details) +{ + JSValue ret; + JSAtom code_atom = JS_NewAtom(ctx, code); + JSAtom tag_atom = JS_NewAtom(ctx, tag); + + if (code_atom == JS_ATOM_NULL || tag_atom == JS_ATOM_NULL) { + if (code_atom != JS_ATOM_NULL) + JS_FreeAtom(ctx, code_atom); + if (tag_atom != JS_ATOM_NULL) + JS_FreeAtom(ctx, tag_atom); + return JS_EXCEPTION; + } + + ret = JS_ThrowHostError(ctx, code_atom, tag_atom, details); + JS_FreeAtom(ctx, code_atom); + JS_FreeAtom(ctx, tag_atom); + return ret; +} + +JSValue JS_ThrowHostError(JSContext *ctx, JSAtom code_atom, JSAtom tag_atom, JSValueConst details) +{ + JSValue obj, name, msg, code_val, tag_val, details_val; + + obj = JS_NewError(ctx); + if (JS_IsException(obj)) + return JS_EXCEPTION; + + name = JS_NewString(ctx, "HostError"); + msg = JS_AtomToString(ctx, tag_atom); + code_val = JS_AtomToString(ctx, code_atom); + tag_val = JS_DupValue(ctx, msg); + details_val = JS_DupValue(ctx, details); + + if (JS_IsException(name) || JS_IsException(msg) || JS_IsException(code_val) || + JS_IsException(tag_val) || JS_IsException(details_val)) { + if (!JS_IsException(name)) + JS_FreeValue(ctx, name); + if (!JS_IsException(msg)) + JS_FreeValue(ctx, msg); + if (!JS_IsException(code_val)) + JS_FreeValue(ctx, code_val); + if (!JS_IsException(tag_val)) + JS_FreeValue(ctx, tag_val); + if (!JS_IsException(details_val)) + JS_FreeValue(ctx, details_val); + JS_FreeValue(ctx, obj); + return JS_EXCEPTION; + } + + JS_DefinePropertyValueStr(ctx, obj, "name", name, + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + JS_DefinePropertyValueStr(ctx, obj, "message", msg, + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + JS_DefinePropertyValueStr(ctx, obj, "code", code_val, + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + JS_DefinePropertyValueStr(ctx, obj, "tag", tag_val, + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + JS_DefinePropertyValueStr(ctx, obj, "details", details_val, + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + JS_Throw(ctx, obj); + return JS_EXCEPTION; +} + +JSValue JS_ThrowHostTransportError(JSContext *ctx) +{ + return js_throw_host_error_str(ctx, + JS_HOST_ERROR_CODE_TRANSPORT, + JS_HOST_ERROR_TAG_TRANSPORT, + JS_UNDEFINED); +} + +static JSValue js_throw_host_envelope_invalid(JSContext *ctx) +{ + return js_throw_host_error_str(ctx, + JS_HOST_ERROR_CODE_ENVELOPE_INVALID, + JS_HOST_ERROR_TAG_ENVELOPE_INVALID, + JS_UNDEFINED); +} + +void JS_FreeHostResponse(JSContext *ctx, JSHostResponse *resp) +{ + if (!ctx || !resp) + return; + + if (!JS_IsUndefined(resp->ok)) + JS_FreeValue(ctx, resp->ok); + if (!JS_IsUndefined(resp->err_details)) + JS_FreeValue(ctx, resp->err_details); + if (resp->err_code_atom != JS_ATOM_NULL) + JS_FreeAtom(ctx, resp->err_code_atom); + if (resp->err_tag_atom != JS_ATOM_NULL) + JS_FreeAtom(ctx, resp->err_tag_atom); + + resp->is_error = 0; + resp->units = 0; + resp->ok = JS_UNDEFINED; + resp->err_details = JS_UNDEFINED; + resp->err_code_atom = JS_ATOM_NULL; + resp->err_tag_atom = JS_ATOM_NULL; +} + +int JS_ParseHostResponse(JSContext *ctx, + const uint8_t *data, + size_t length, + const JSHostResponseValidation *validation, + JSHostResponse *out) +{ + JSHostResponse tmp; + JSValue envelope = JS_UNDEFINED; + JSValue ok_val = JS_UNDEFINED; + JSValue err_val = JS_UNDEFINED; + JSValue units_val = JS_UNDEFINED; + JSValue err_code_val = JS_UNDEFINED; + JSValue err_details_val = JS_UNDEFINED; + JSPropertyEnum *props = NULL; + JSPropertyEnum *err_props = NULL; + uint32_t props_len = 0; + uint32_t err_props_len = 0; + JSAtom ok_atom = JS_ATOM_NULL; + JSAtom err_atom = JS_ATOM_NULL; + JSAtom units_atom = JS_ATOM_NULL; + JSAtom code_atom = JS_ATOM_NULL; + JSAtom details_atom = JS_ATOM_NULL; + BOOL has_pending_exception = FALSE; + int ret = -1; + + if (!ctx || !validation || !out) + return -1; + + if (validation->error_count > 0 && !validation->errors) { + JS_ThrowTypeError(ctx, "host response errors table is required"); + return -1; + } + + tmp.is_error = 0; + tmp.units = 0; + tmp.ok = JS_UNDEFINED; + tmp.err_code_atom = JS_ATOM_NULL; + tmp.err_tag_atom = JS_ATOM_NULL; + tmp.err_details = JS_UNDEFINED; + + if (!data && length > 0) { + js_throw_host_envelope_invalid(ctx); + return -1; + } + + if (length > JS_DV_LIMIT_DEFAULTS.max_encoded_bytes) { + js_throw_host_envelope_invalid(ctx); + return -1; + } + + envelope = JS_DecodeDV(ctx, data, length, &JS_DV_LIMIT_DEFAULTS); + if (JS_IsException(envelope)) { + has_pending_exception = TRUE; + goto envelope_invalid; + } + + if (!JS_IsObject(envelope)) + goto envelope_invalid; + + ok_atom = JS_NewAtom(ctx, "ok"); + err_atom = JS_NewAtom(ctx, "err"); + units_atom = JS_NewAtom(ctx, "units"); + code_atom = JS_NewAtom(ctx, "code"); + details_atom = JS_NewAtom(ctx, "details"); + if (ok_atom == JS_ATOM_NULL || err_atom == JS_ATOM_NULL || units_atom == JS_ATOM_NULL || + code_atom == JS_ATOM_NULL || details_atom == JS_ATOM_NULL) { + has_pending_exception = TRUE; + goto envelope_invalid; + } + + if (JS_GetOwnPropertyNames(ctx, &props, &props_len, envelope, + JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY) < 0) { + has_pending_exception = TRUE; + goto envelope_invalid; + } + + for (uint32_t i = 0; i < props_len; i++) { + JSAtom atom = props[i].atom; + if (atom != ok_atom && atom != err_atom && atom != units_atom) { + goto envelope_invalid; + } + } + + ok_val = JS_GetProperty(ctx, envelope, ok_atom); + if (JS_IsException(ok_val)) { + has_pending_exception = TRUE; + goto envelope_invalid; + } + + err_val = JS_GetProperty(ctx, envelope, err_atom); + if (JS_IsException(err_val)) { + has_pending_exception = TRUE; + goto envelope_invalid; + } + + units_val = JS_GetProperty(ctx, envelope, units_atom); + if (JS_IsException(units_val)) { + has_pending_exception = TRUE; + goto envelope_invalid; + } + + BOOL has_ok = !JS_IsUndefined(ok_val); + BOOL has_err = !JS_IsUndefined(err_val); + + if ((has_ok && has_err) || (!has_ok && !has_err)) + goto envelope_invalid; + + if (JS_IsUndefined(units_val)) + goto envelope_invalid; + + if (!JS_IsNumber(units_val)) + goto envelope_invalid; + + double units_d; + if (JS_ToFloat64(ctx, &units_d, units_val)) { + has_pending_exception = TRUE; + goto envelope_invalid; + } + + if (!isfinite(units_d)) + goto envelope_invalid; + + if ((units_d == 0.0 && signbit(units_d)) || units_d < 0.0 || + units_d > (double)UINT32_MAX || units_d > (double)validation->max_units) + goto envelope_invalid; + + if (floor(units_d) != units_d) + goto envelope_invalid; + + tmp.units = (uint32_t)units_d; + + if (has_ok) { + tmp.is_error = 0; + tmp.ok = ok_val; + ok_val = JS_UNDEFINED; + } else { + if (!JS_IsObject(err_val)) + goto envelope_invalid; + + if (JS_GetOwnPropertyNames(ctx, &err_props, &err_props_len, err_val, + JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY) < 0) { + has_pending_exception = TRUE; + goto envelope_invalid; + } + + for (uint32_t i = 0; i < err_props_len; i++) { + JSAtom atom = err_props[i].atom; + if (atom != code_atom && atom != details_atom) { + goto envelope_invalid; + } + } + + err_code_val = JS_GetProperty(ctx, err_val, code_atom); + if (JS_IsException(err_code_val)) { + has_pending_exception = TRUE; + goto envelope_invalid; + } + + if (JS_IsUndefined(err_code_val)) + goto envelope_invalid; + + if (!JS_IsString(err_code_val)) + goto envelope_invalid; + + const char *code_str = JS_ToCString(ctx, err_code_val); + if (!code_str) { + has_pending_exception = TRUE; + goto envelope_invalid; + } + + JSAtom code_atom_val = JS_NewAtom(ctx, code_str); + JS_FreeCString(ctx, code_str); + if (code_atom_val == JS_ATOM_NULL) { + has_pending_exception = TRUE; + goto envelope_invalid; + } + + JSAtom tag_atom_val = JS_ATOM_NULL; + for (size_t i = 0; i < validation->error_count; i++) { + if (validation->errors[i].code_atom == code_atom_val) { + tag_atom_val = validation->errors[i].tag_atom; + break; + } + } + + if (tag_atom_val == JS_ATOM_NULL) { + JS_FreeAtom(ctx, code_atom_val); + goto envelope_invalid; + } + + tmp.is_error = 1; + tmp.err_code_atom = code_atom_val; + tmp.err_tag_atom = JS_DupAtom(ctx, tag_atom_val); + if (tmp.err_tag_atom == JS_ATOM_NULL) { + has_pending_exception = TRUE; + goto envelope_invalid; + } + + err_details_val = JS_GetProperty(ctx, err_val, details_atom); + if (JS_IsException(err_details_val)) { + has_pending_exception = TRUE; + goto envelope_invalid; + } + + if (!JS_IsUndefined(err_details_val)) { + tmp.err_details = err_details_val; + err_details_val = JS_UNDEFINED; + } + JS_FreeValue(ctx, err_code_val); + err_code_val = JS_UNDEFINED; + } + + ret = 0; + *out = tmp; + tmp.ok = JS_UNDEFINED; + tmp.err_details = JS_UNDEFINED; + tmp.err_code_atom = JS_ATOM_NULL; + tmp.err_tag_atom = JS_ATOM_NULL; + goto done; + +envelope_invalid: + if (has_pending_exception) { + JSValue pending = JS_GetException(ctx); + if (!JS_IsException(pending)) { + BOOL is_out_of_gas = FALSE; + + if (JS_IsError(ctx, pending)) { + JSValue code_val = JS_GetPropertyStr(ctx, pending, "code"); + if (JS_IsException(code_val)) { + JSValue exc2 = JS_GetException(ctx); + if (!JS_IsException(exc2)) + JS_FreeValue(ctx, exc2); + JS_FreeValue(ctx, code_val); + } else { + const char *code_str = JS_ToCString(ctx, code_val); + if (code_str) { + if (strcmp(code_str, "OOG") == 0) + is_out_of_gas = TRUE; + JS_FreeCString(ctx, code_str); + } + JS_FreeValue(ctx, code_val); + } + } + + if (is_out_of_gas) { + JS_Throw(ctx, pending); + JS_SetUncatchableException(ctx, TRUE); + ret = -1; + JS_FreeHostResponse(ctx, &tmp); + goto done; + } + JS_FreeValue(ctx, pending); + } + } + + js_throw_host_envelope_invalid(ctx); + ret = -1; + JS_FreeHostResponse(ctx, &tmp); + +done: + if (props) + JS_FreePropertyEnum(ctx, props, props_len); + if (err_props) + JS_FreePropertyEnum(ctx, err_props, err_props_len); + if (ok_atom != JS_ATOM_NULL) + JS_FreeAtom(ctx, ok_atom); + if (err_atom != JS_ATOM_NULL) + JS_FreeAtom(ctx, err_atom); + if (units_atom != JS_ATOM_NULL) + JS_FreeAtom(ctx, units_atom); + if (code_atom != JS_ATOM_NULL) + JS_FreeAtom(ctx, code_atom); + if (details_atom != JS_ATOM_NULL) + JS_FreeAtom(ctx, details_atom); + if (!JS_IsUndefined(envelope)) + JS_FreeValue(ctx, envelope); + if (!JS_IsUndefined(ok_val)) + JS_FreeValue(ctx, ok_val); + if (!JS_IsUndefined(err_val)) + JS_FreeValue(ctx, err_val); + if (!JS_IsUndefined(units_val)) + JS_FreeValue(ctx, units_val); + if (!JS_IsUndefined(err_code_val)) + JS_FreeValue(ctx, err_code_val); + if (!JS_IsUndefined(err_details_val)) + JS_FreeValue(ctx, err_details_val); + return ret; +} diff --git a/quickjs-host.h b/quickjs-host.h new file mode 100644 index 000000000..3ab2cbe10 --- /dev/null +++ b/quickjs-host.h @@ -0,0 +1,36 @@ +#ifndef QUICKJS_HOST_H +#define QUICKJS_HOST_H + +#include "quickjs.h" + +typedef struct JSHostErrorEntry { + JSAtom code_atom; + JSAtom tag_atom; +} JSHostErrorEntry; + +typedef struct JSHostResponseValidation { + uint32_t max_units; + const JSHostErrorEntry *errors; + size_t error_count; +} JSHostResponseValidation; + +typedef struct JSHostResponse { + int is_error; + uint32_t units; + JSValue ok; + JSAtom err_code_atom; + JSAtom err_tag_atom; + JSValue err_details; +} JSHostResponse; + +/* Host response envelope helpers (T-039). */ +JSValue JS_ThrowHostError(JSContext *ctx, JSAtom code_atom, JSAtom tag_atom, JSValueConst details); +JSValue JS_ThrowHostTransportError(JSContext *ctx); +int JS_ParseHostResponse(JSContext *ctx, + const uint8_t *data, + size_t length, + const JSHostResponseValidation *validation, + JSHostResponse *out); +void JS_FreeHostResponse(JSContext *ctx, JSHostResponse *resp); + +#endif /* QUICKJS_HOST_H */ diff --git a/quickjs.c b/quickjs.c index e54a2fc4c..3cc7d35b9 100644 --- a/quickjs.c +++ b/quickjs.c @@ -44,6 +44,7 @@ #include "list.h" #include "quickjs-internal.h" #include "quickjs.h" +#include "quickjs-host.h" #include "libregexp.h" #include "libunicode.h" #include "dtoa.h" @@ -3248,7 +3249,7 @@ int JS_HostCall(JSContext *ctx, return -1; if (resp_len == JS_HOST_CALL_TRANSPORT_ERROR || resp_len > resp_capacity) { - JS_ThrowTypeError(ctx, "host_call transport failed"); + JS_ThrowHostTransportError(ctx); return -1; } From 54c408db3f8fc2903970b08951e3edc3cc3d1aee Mon Sep 17 00:00:00 2001 From: Mateusz Jonak Date: Tue, 16 Dec 2025 21:42:40 +0100 Subject: [PATCH 17/19] feat(quickjs-host): parse Host.v1 manifest and install deterministic host bindings (T-040) - add DV manifest parser + validation (sorted fn_id, js_path collision checks, reserved error codes) - generate/install Host.v1 namespaces + per-function CFunction wrappers with pre/post gas charging - validate args/return types (string/null/dv) and enforce per-arg utf8 limits - expose host lifecycle API: JS_InitHostFromManifest / JS_FreeHostManifest - init deterministic contexts from manifest and free manifest state on JS_FreeContext - prevent extensions on Host namespaces created from the manifest Refs: T-040 --- quickjs-host.c | 1419 ++++++++++++++++++++++++++++++++++++++++++++++++ quickjs-host.h | 4 + quickjs.c | 11 +- 3 files changed, 1431 insertions(+), 3 deletions(-) diff --git a/quickjs-host.c b/quickjs-host.c index d2295fe94..0deeaa91b 100644 --- a/quickjs-host.c +++ b/quickjs-host.c @@ -2,6 +2,7 @@ #include "quickjs-host.h" #include "quickjs-internal.h" #include +#include #include #define JS_HOST_ERROR_CODE_TRANSPORT "HOST_TRANSPORT" @@ -399,3 +400,1421 @@ int JS_ParseHostResponse(JSContext *ctx, JS_FreeValue(ctx, err_details_val); return ret; } + +/* ------------------------------------------------------------------------- */ +/* Host manifest parsing and Host.v1 generation (T-040) */ + +extern void *js_malloc(JSContext *ctx, size_t size); +extern void js_free(JSContext *ctx, void *ptr); +extern void *js_realloc(JSContext *ctx, void *ptr, size_t size); + +typedef enum { + JS_HOST_SCHEMA_STRING, + JS_HOST_SCHEMA_DV, + JS_HOST_SCHEMA_NULL, +} JSHostSchemaType; + +typedef enum { + JS_HOST_EFFECT_READ, + JS_HOST_EFFECT_EMIT, + JS_HOST_EFFECT_MUTATE, +} JSHostEffect; + +typedef struct { + JSHostSchemaType type; + uint32_t utf8_max; /* 0 when not provided */ +} JSHostArgDef; + +typedef struct { + uint32_t fn_id; + size_t path_len; + char **path_segments; + char *name; + uint32_t arity; + JSHostEffect effect; + JSHostArgDef *args; + JSHostSchemaType return_type; + uint32_t gas_base; + uint32_t gas_k_arg_bytes; + uint32_t gas_k_ret_bytes; + uint32_t gas_k_units; + char *gas_schedule_id; + uint32_t max_request_bytes; + uint32_t max_response_bytes; + uint32_t max_units; + JSHostErrorEntry *errors; + size_t error_count; +} JSHostFunctionDef; + +struct JSHostManifest { + JSHostFunctionDef *functions; + size_t function_count; +}; + +typedef struct JSHostManifestNode { + JSContext *ctx; + JSHostManifest manifest; + struct JSHostManifestNode *next; +} JSHostManifestNode; + +static JSHostManifestNode *js_host_manifest_list = NULL; + +static JSHostManifest *js_host_find_manifest(JSContext *ctx) +{ + JSHostManifestNode *node = js_host_manifest_list; + while (node) { + if (node->ctx == ctx) + return &node->manifest; + node = node->next; + } + return NULL; +} + +static void js_host_free_function(JSContext *ctx, JSHostFunctionDef *fn) +{ + if (!fn) + return; + + if (fn->path_segments) { + for (size_t i = 0; i < fn->path_len; i++) { + if (fn->path_segments[i]) + js_free(ctx, fn->path_segments[i]); + } + js_free(ctx, fn->path_segments); + } + + if (fn->args) + js_free(ctx, fn->args); + + if (fn->errors) { + for (size_t i = 0; i < fn->error_count; i++) { + if (fn->errors[i].code_atom != JS_ATOM_NULL) + JS_FreeAtom(ctx, fn->errors[i].code_atom); + if (fn->errors[i].tag_atom != JS_ATOM_NULL) + JS_FreeAtom(ctx, fn->errors[i].tag_atom); + } + js_free(ctx, fn->errors); + } + + if (fn->gas_schedule_id) + js_free(ctx, fn->gas_schedule_id); + + if (fn->name) + js_free(ctx, fn->name); + + memset(fn, 0, sizeof(*fn)); +} + +static void js_host_manifest_clear(JSContext *ctx, JSHostManifest *manifest) +{ + if (!manifest) + return; + + if (manifest->functions) { + for (size_t i = 0; i < manifest->function_count; i++) + js_host_free_function(ctx, &manifest->functions[i]); + js_free(ctx, manifest->functions); + } + manifest->functions = NULL; + manifest->function_count = 0; +} + +void JS_FreeHostManifest(JSContext *ctx) +{ + JSHostManifestNode *prev = NULL; + JSHostManifestNode *node = js_host_manifest_list; + + while (node) { + if (node->ctx == ctx) { + if (prev) + prev->next = node->next; + else + js_host_manifest_list = node->next; + + js_host_manifest_clear(ctx, &node->manifest); + js_free(ctx, node); + return; + } + prev = node; + node = node->next; + } +} + +static int js_host_manifest_error(JSContext *ctx, const char *path, const char *message) +{ + if (path && message) + JS_ThrowTypeError(ctx, "abi manifest invalid: %s (%s)", message, path); + else if (message) + JS_ThrowTypeError(ctx, "abi manifest invalid: %s", message); + else + JS_ThrowTypeError(ctx, "abi manifest invalid"); + return -1; +} + +static int js_host_expect_object(JSContext *ctx, JSValueConst val, const char *path) +{ + if (!JS_IsObject(val) || JS_IsArray(ctx, val)) + return js_host_manifest_error(ctx, path, "expected object"); + return 0; +} + +static int js_host_check_keys(JSContext *ctx, + JSValueConst obj, + const char **required, + size_t required_len, + const char **optional, + size_t optional_len, + const char *path) +{ + JSPropertyEnum *props = NULL; + uint32_t props_len = 0; + uint8_t *seen = NULL; + int ret = -1; + + if (JS_GetOwnPropertyNames(ctx, &props, &props_len, obj, + JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY) < 0) + return -1; + + if (required_len > 0) { + seen = js_malloc(ctx, required_len); + if (!seen) { + JS_FreePropertyEnum(ctx, props, props_len); + return -1; + } + memset(seen, 0, required_len); + } + + for (uint32_t i = 0; i < props_len; i++) { + const char *name = JS_AtomToCString(ctx, props[i].atom); + BOOL allowed = FALSE; + + if (!name) + goto cleanup; + + for (size_t j = 0; j < required_len; j++) { + if (strcmp(name, required[j]) == 0) { + allowed = TRUE; + if (seen) + seen[j] = 1; + break; + } + } + + if (!allowed) { + for (size_t j = 0; j < optional_len; j++) { + if (strcmp(name, optional[j]) == 0) { + allowed = TRUE; + break; + } + } + } + + JS_FreeCString(ctx, name); + + if (!allowed) { + js_host_manifest_error(ctx, path, "unknown field"); + goto cleanup; + } + } + + for (size_t j = 0; j < required_len; j++) { + if (seen && !seen[j]) { + js_host_manifest_error(ctx, path, "missing required field"); + goto cleanup; + } + } + + ret = 0; + +cleanup: + if (props) + JS_FreePropertyEnum(ctx, props, props_len); + if (seen) + js_free(ctx, seen); + return ret; +} + +static int js_host_copy_non_empty_string(JSContext *ctx, + JSValueConst val, + const char *path, + char **out) +{ + const char *tmp; + char *copy; + + if (!JS_IsString(val)) + return js_host_manifest_error(ctx, path, "expected string"); + + tmp = JS_ToCString(ctx, val); + if (!tmp) + return -1; + + if (tmp[0] == '\0') { + JS_FreeCString(ctx, tmp); + return js_host_manifest_error(ctx, path, "string must be non-empty"); + } + + copy = js_malloc(ctx, strlen(tmp) + 1); + if (!copy) { + JS_FreeCString(ctx, tmp); + return -1; + } + + strcpy(copy, tmp); + JS_FreeCString(ctx, tmp); + *out = copy; + return 0; +} + +static int js_host_validate_uint32(JSContext *ctx, + JSValueConst val, + const char *path, + uint32_t min, + uint32_t max, + uint32_t *out) +{ + double d; + + if (!JS_IsNumber(val)) + return js_host_manifest_error(ctx, path, "expected integer"); + + if (JS_ToFloat64(ctx, &d, val)) + return -1; + + if (!isfinite(d) || floor(d) != d || (d == 0.0 && signbit(d)) || d < (double)min || d > (double)max) + return js_host_manifest_error(ctx, path, "value out of range"); + + *out = (uint32_t)d; + return 0; +} + +static int js_host_array_length(JSContext *ctx, JSValueConst arr, size_t *out_len) +{ + JSValue len_val = JS_UNDEFINED; + uint32_t len32 = 0; + int ret = -1; + + len_val = JS_GetPropertyStr(ctx, arr, "length"); + if (JS_IsException(len_val)) + return -1; + + if (JS_ToUint32(ctx, &len32, len_val)) { + JS_FreeValue(ctx, len_val); + return -1; + } + + JS_FreeValue(ctx, len_val); + *out_len = (size_t)len32; + ret = 0; + return ret; +} + +static int js_host_validate_schema(JSContext *ctx, + JSValueConst schema_val, + const char *path, + JSHostSchemaType *out) +{ + const char *required[] = {"type"}; + JSValue type_val = JS_UNDEFINED; + const char *type_str; + int ret = -1; + + if (js_host_expect_object(ctx, schema_val, path)) + return -1; + + if (js_host_check_keys(ctx, schema_val, required, 1, NULL, 0, path)) + return -1; + + type_val = JS_GetPropertyStr(ctx, schema_val, "type"); + if (JS_IsException(type_val)) + goto done; + + if (!JS_IsString(type_val)) { + js_host_manifest_error(ctx, path, "schema.type must be a string"); + goto done; + } + + type_str = JS_ToCString(ctx, type_val); + if (!type_str) + goto done; + + if (strcmp(type_str, "string") == 0) { + *out = JS_HOST_SCHEMA_STRING; + } else if (strcmp(type_str, "dv") == 0) { + *out = JS_HOST_SCHEMA_DV; + } else if (strcmp(type_str, "null") == 0) { + *out = JS_HOST_SCHEMA_NULL; + } else { + js_host_manifest_error(ctx, path, "unsupported schema type"); + JS_FreeCString(ctx, type_str); + goto done; + } + + JS_FreeCString(ctx, type_str); + ret = 0; + +done: + if (!JS_IsUndefined(type_val)) + JS_FreeValue(ctx, type_val); + return ret; +} + +static int js_host_validate_js_path(JSContext *ctx, + JSValueConst path_val, + const char *path_desc, + JSHostFunctionDef *out_fn) +{ + size_t len = 0; + char **segments = NULL; + JSValue entry = JS_UNDEFINED; + int ret = -1; + + if (!JS_IsArray(ctx, path_val)) + return js_host_manifest_error(ctx, path_desc, "js_path must be an array"); + + if (js_host_array_length(ctx, path_val, &len)) + return -1; + + if (len == 0) + return js_host_manifest_error(ctx, path_desc, "js_path must contain at least one segment"); + + segments = js_malloc(ctx, sizeof(char *) * len); + if (!segments) + return -1; + memset(segments, 0, sizeof(char *) * len); + + for (size_t i = 0; i < len; i++) { + char key_buf[64]; + snprintf(key_buf, sizeof(key_buf), "%s[%zu]", path_desc, i); + + entry = JS_GetPropertyUint32(ctx, path_val, (uint32_t)i); + if (JS_IsException(entry)) + goto cleanup; + + if (js_host_copy_non_empty_string(ctx, entry, key_buf, &segments[i])) + goto cleanup; + + JS_FreeValue(ctx, entry); + entry = JS_UNDEFINED; + + const char *seg = segments[i]; + for (const char *p = seg; *p; p++) { + char c = *p; + if (!((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || + c == '_' || c == '-')) { + js_host_manifest_error(ctx, key_buf, "js_path segment must match [A-Za-z0-9_-]+"); + goto cleanup; + } + } + + if (strcmp(seg, "__proto__") == 0 || strcmp(seg, "prototype") == 0 || strcmp(seg, "constructor") == 0) { + js_host_manifest_error(ctx, key_buf, "js_path segment is forbidden"); + goto cleanup; + } + } + + out_fn->path_segments = segments; + out_fn->path_len = len; + ret = 0; + +cleanup: + if (ret != 0 && segments) { + for (size_t i = 0; i < len; i++) { + if (segments[i]) + js_free(ctx, segments[i]); + } + js_free(ctx, segments); + } + if (!JS_IsUndefined(entry)) + JS_FreeValue(ctx, entry); + return ret; +} + +static int js_host_validate_error_codes(JSContext *ctx, + JSValueConst errors_val, + const char *path, + JSHostFunctionDef *out_fn) +{ + JSValue entry = JS_UNDEFINED; + JSValue code_val = JS_UNDEFINED; + JSValue tag_val = JS_UNDEFINED; + char *code_str = NULL; + char *tag_str = NULL; + int ret = -1; + + if (!JS_IsArray(ctx, errors_val)) + return js_host_manifest_error(ctx, path, "error_codes must be an array"); + + if (js_host_array_length(ctx, errors_val, &out_fn->error_count)) + return -1; + + if (out_fn->error_count == 0) { + out_fn->errors = NULL; + return 0; + } + + out_fn->errors = js_malloc(ctx, sizeof(JSHostErrorEntry) * out_fn->error_count); + if (!out_fn->errors) + return -1; + memset(out_fn->errors, 0, sizeof(JSHostErrorEntry) * out_fn->error_count); + + for (size_t i = 0; i < out_fn->error_count; i++) { + char entry_path[96]; + snprintf(entry_path, sizeof(entry_path), "%s[%zu]", path, i); + + entry = JS_GetPropertyUint32(ctx, errors_val, (uint32_t)i); + if (JS_IsException(entry)) + goto cleanup; + + const char *required[] = {"code", "tag"}; + if (js_host_expect_object(ctx, entry, entry_path) || + js_host_check_keys(ctx, entry, required, 2, NULL, 0, entry_path)) + goto cleanup; + + code_val = JS_GetPropertyStr(ctx, entry, "code"); + if (JS_IsException(code_val)) + goto cleanup; + if (js_host_copy_non_empty_string(ctx, code_val, entry_path, &code_str)) + goto cleanup; + + tag_val = JS_GetPropertyStr(ctx, entry, "tag"); + if (JS_IsException(tag_val)) + goto cleanup; + if (js_host_copy_non_empty_string(ctx, tag_val, entry_path, &tag_str)) + goto cleanup; + + if (strcmp(code_str, JS_HOST_ERROR_CODE_TRANSPORT) == 0 || + strcmp(code_str, JS_HOST_ERROR_CODE_ENVELOPE_INVALID) == 0) { + js_host_manifest_error(ctx, entry_path, "reserved error code"); + goto cleanup; + } + + if (i > 0) { + JSAtom prev_atom = out_fn->errors[i - 1].code_atom; + const char *prev = JS_AtomToCString(ctx, prev_atom); + if (!prev) + goto cleanup; + int cmp = strcmp(prev, code_str); + JS_FreeCString(ctx, prev); + if (cmp >= 0) { + js_host_manifest_error(ctx, path, "error_codes must be sorted and unique"); + goto cleanup; + } + } + + out_fn->errors[i].code_atom = JS_NewAtom(ctx, code_str); + out_fn->errors[i].tag_atom = JS_NewAtom(ctx, tag_str); + if (out_fn->errors[i].code_atom == JS_ATOM_NULL || + out_fn->errors[i].tag_atom == JS_ATOM_NULL) + goto cleanup; + + js_free(ctx, code_str); + js_free(ctx, tag_str); + code_str = NULL; + tag_str = NULL; + + JS_FreeValue(ctx, entry); + JS_FreeValue(ctx, code_val); + JS_FreeValue(ctx, tag_val); + entry = code_val = tag_val = JS_UNDEFINED; + } + + ret = 0; + +cleanup: + if (code_str) + js_free(ctx, code_str); + if (tag_str) + js_free(ctx, tag_str); + if (!JS_IsUndefined(entry)) + JS_FreeValue(ctx, entry); + if (!JS_IsUndefined(code_val)) + JS_FreeValue(ctx, code_val); + if (!JS_IsUndefined(tag_val)) + JS_FreeValue(ctx, tag_val); + if (ret != 0 && out_fn->errors) { + for (size_t i = 0; i < out_fn->error_count; i++) { + if (out_fn->errors[i].code_atom != JS_ATOM_NULL) + JS_FreeAtom(ctx, out_fn->errors[i].code_atom); + if (out_fn->errors[i].tag_atom != JS_ATOM_NULL) + JS_FreeAtom(ctx, out_fn->errors[i].tag_atom); + } + js_free(ctx, out_fn->errors); + out_fn->errors = NULL; + } + return ret; +} + +static int js_host_validate_limits(JSContext *ctx, + JSValueConst limits_val, + const char *path, + JSHostFunctionDef *out_fn) +{ + const char *required[] = {"max_request_bytes", "max_response_bytes", "max_units"}; + const char *optional[] = {"arg_utf8_max"}; + JSValue max_req = JS_UNDEFINED; + JSValue max_resp = JS_UNDEFINED; + JSValue max_units = JS_UNDEFINED; + JSValue arg_utf8 = JS_UNDEFINED; + int ret = -1; + + if (js_host_expect_object(ctx, limits_val, path)) + return -1; + + if (js_host_check_keys(ctx, limits_val, required, 3, optional, 1, path)) + return -1; + + max_req = JS_GetPropertyStr(ctx, limits_val, "max_request_bytes"); + if (JS_IsException(max_req)) + goto done; + if (js_host_validate_uint32(ctx, max_req, path, 1, JS_DV_LIMIT_DEFAULTS.max_encoded_bytes, &out_fn->max_request_bytes)) + goto done; + + max_resp = JS_GetPropertyStr(ctx, limits_val, "max_response_bytes"); + if (JS_IsException(max_resp)) + goto done; + if (js_host_validate_uint32(ctx, max_resp, path, 1, JS_DV_LIMIT_DEFAULTS.max_encoded_bytes, &out_fn->max_response_bytes)) + goto done; + + max_units = JS_GetPropertyStr(ctx, limits_val, "max_units"); + if (JS_IsException(max_units)) + goto done; + if (js_host_validate_uint32(ctx, max_units, path, 0, UINT32_MAX, &out_fn->max_units)) + goto done; + + arg_utf8 = JS_GetPropertyStr(ctx, limits_val, "arg_utf8_max"); + if (JS_IsException(arg_utf8)) + goto done; + + if (!JS_IsUndefined(arg_utf8)) { + size_t len = 0; + if (!JS_IsArray(ctx, arg_utf8)) { + js_host_manifest_error(ctx, path, "arg_utf8_max must be an array when present"); + goto done; + } + if (js_host_array_length(ctx, arg_utf8, &len)) + goto done; + if (len != out_fn->arity) { + js_host_manifest_error(ctx, path, "arg_utf8_max length must equal arity"); + goto done; + } + + for (size_t i = 0; i < out_fn->arity; i++) { + JSValue limit_val = JS_GetPropertyUint32(ctx, arg_utf8, (uint32_t)i); + if (JS_IsException(limit_val)) + goto done; + uint32_t utf8_limit = 0; + char idx_path[96]; + snprintf(idx_path, sizeof(idx_path), "%s[%zu]", path, i); + + if (js_host_validate_uint32(ctx, + limit_val, + idx_path, + 1, + JS_DV_LIMIT_DEFAULTS.max_string_bytes, + &utf8_limit)) { + JS_FreeValue(ctx, limit_val); + goto done; + } + + JS_FreeValue(ctx, limit_val); + + if (out_fn->args[i].type != JS_HOST_SCHEMA_STRING) { + js_host_manifest_error(ctx, path, "arg_utf8_max may only be used with string arguments"); + goto done; + } + out_fn->args[i].utf8_max = utf8_limit; + } + } + + ret = 0; + +done: + if (!JS_IsUndefined(max_req)) + JS_FreeValue(ctx, max_req); + if (!JS_IsUndefined(max_resp)) + JS_FreeValue(ctx, max_resp); + if (!JS_IsUndefined(max_units)) + JS_FreeValue(ctx, max_units); + if (!JS_IsUndefined(arg_utf8)) + JS_FreeValue(ctx, arg_utf8); + return ret; +} + +static int js_host_build_function_name(JSContext *ctx, JSHostFunctionDef *fn) +{ + const char *prefix = "Host.v1"; + size_t total = strlen(prefix) + 1; /* null terminator */ + + for (size_t i = 0; i < fn->path_len; i++) + total += 1 + strlen(fn->path_segments[i]); /* dot + segment */ + + fn->name = js_malloc(ctx, total); + if (!fn->name) + return -1; + + strcpy(fn->name, prefix); + for (size_t i = 0; i < fn->path_len; i++) { + strcat(fn->name, "."); + strcat(fn->name, fn->path_segments[i]); + } + return 0; +} + +static int js_host_validate_function(JSContext *ctx, + JSValueConst fn_val, + const char *path, + JSHostFunctionDef *out_fn) +{ + const char *required[] = {"fn_id", "js_path", "effect", "arity", "arg_schema", "return_schema", "gas", "limits", "error_codes"}; + JSValue fn_id = JS_UNDEFINED; + JSValue js_path = JS_UNDEFINED; + JSValue effect = JS_UNDEFINED; + JSValue arity = JS_UNDEFINED; + JSValue arg_schema = JS_UNDEFINED; + JSValue return_schema = JS_UNDEFINED; + JSValue gas = JS_UNDEFINED; + JSValue limits = JS_UNDEFINED; + JSValue error_codes = JS_UNDEFINED; + const char *eff_str = NULL; + char *schedule_id_str = NULL; + int ret = -1; + + if (js_host_expect_object(ctx, fn_val, path)) + return -1; + + if (js_host_check_keys(ctx, fn_val, required, sizeof(required) / sizeof(required[0]), NULL, 0, path)) + return -1; + + fn_id = JS_GetPropertyStr(ctx, fn_val, "fn_id"); + if (JS_IsException(fn_id)) + goto done; + if (js_host_validate_uint32(ctx, fn_id, path, 1, UINT32_MAX, &out_fn->fn_id)) + goto done; + + js_path = JS_GetPropertyStr(ctx, fn_val, "js_path"); + if (JS_IsException(js_path)) + goto done; + if (js_host_validate_js_path(ctx, js_path, "js_path", out_fn)) + goto done; + + effect = JS_GetPropertyStr(ctx, fn_val, "effect"); + if (JS_IsException(effect)) + goto done; + if (!JS_IsString(effect)) { + js_host_manifest_error(ctx, path, "effect must be a string"); + goto done; + } + eff_str = JS_ToCString(ctx, effect); + if (!eff_str) + goto done; + if (strcmp(eff_str, "READ") == 0) { + out_fn->effect = JS_HOST_EFFECT_READ; + } else if (strcmp(eff_str, "EMIT") == 0) { + out_fn->effect = JS_HOST_EFFECT_EMIT; + } else if (strcmp(eff_str, "MUTATE") == 0) { + out_fn->effect = JS_HOST_EFFECT_MUTATE; + } else { + js_host_manifest_error(ctx, path, "unsupported effect"); + goto done; + } + JS_FreeCString(ctx, eff_str); + eff_str = NULL; + + arity = JS_GetPropertyStr(ctx, fn_val, "arity"); + if (JS_IsException(arity)) + goto done; + if (js_host_validate_uint32(ctx, arity, path, 0, UINT32_MAX, &out_fn->arity)) + goto done; + + arg_schema = JS_GetPropertyStr(ctx, fn_val, "arg_schema"); + if (JS_IsException(arg_schema)) + goto done; + if (!JS_IsArray(ctx, arg_schema)) { + js_host_manifest_error(ctx, path, "arg_schema must be an array"); + goto done; + } + + size_t arg_len = 0; + if (js_host_array_length(ctx, arg_schema, &arg_len)) + goto done; + if (arg_len != out_fn->arity) { + js_host_manifest_error(ctx, path, "arg_schema length must equal arity"); + goto done; + } + + if (out_fn->arity > 0) { + out_fn->args = js_malloc(ctx, sizeof(JSHostArgDef) * out_fn->arity); + if (!out_fn->args) + goto done; + memset(out_fn->args, 0, sizeof(JSHostArgDef) * out_fn->arity); + } + + for (size_t i = 0; i < out_fn->arity; i++) { + JSValue schema_entry = JS_GetPropertyUint32(ctx, arg_schema, (uint32_t)i); + if (JS_IsException(schema_entry)) + goto done; + char schema_path[96]; + snprintf(schema_path, sizeof(schema_path), "arg_schema[%zu]", i); + if (js_host_validate_schema(ctx, schema_entry, schema_path, &out_fn->args[i].type)) { + JS_FreeValue(ctx, schema_entry); + goto done; + } + JS_FreeValue(ctx, schema_entry); + } + + return_schema = JS_GetPropertyStr(ctx, fn_val, "return_schema"); + if (JS_IsException(return_schema)) + goto done; + if (js_host_validate_schema(ctx, return_schema, "return_schema", &out_fn->return_type)) + goto done; + + gas = JS_GetPropertyStr(ctx, fn_val, "gas"); + if (JS_IsException(gas)) + goto done; + const char *gas_required[] = {"schedule_id", "base", "k_arg_bytes", "k_ret_bytes", "k_units"}; + if (js_host_expect_object(ctx, gas, "gas") || + js_host_check_keys(ctx, gas, gas_required, 5, NULL, 0, "gas")) + goto done; + + JSValue base = JS_GetPropertyStr(ctx, gas, "base"); + JSValue k_arg = JS_GetPropertyStr(ctx, gas, "k_arg_bytes"); + JSValue k_ret = JS_GetPropertyStr(ctx, gas, "k_ret_bytes"); + JSValue k_units = JS_GetPropertyStr(ctx, gas, "k_units"); + JSValue schedule_id = JS_GetPropertyStr(ctx, gas, "schedule_id"); + if (JS_IsException(base) || JS_IsException(k_arg) || JS_IsException(k_ret) || + JS_IsException(k_units) || JS_IsException(schedule_id)) { + JS_FreeValue(ctx, base); + JS_FreeValue(ctx, k_arg); + JS_FreeValue(ctx, k_ret); + JS_FreeValue(ctx, k_units); + JS_FreeValue(ctx, schedule_id); + goto done; + } + + if (js_host_validate_uint32(ctx, base, "gas.base", 0, UINT32_MAX, &out_fn->gas_base) || + js_host_validate_uint32(ctx, k_arg, "gas.k_arg_bytes", 0, UINT32_MAX, &out_fn->gas_k_arg_bytes) || + js_host_validate_uint32(ctx, k_ret, "gas.k_ret_bytes", 0, UINT32_MAX, &out_fn->gas_k_ret_bytes) || + js_host_validate_uint32(ctx, k_units, "gas.k_units", 0, UINT32_MAX, &out_fn->gas_k_units) || + js_host_copy_non_empty_string(ctx, schedule_id, "gas.schedule_id", &schedule_id_str)) { + JS_FreeValue(ctx, base); + JS_FreeValue(ctx, k_arg); + JS_FreeValue(ctx, k_ret); + JS_FreeValue(ctx, k_units); + JS_FreeValue(ctx, schedule_id); + goto done; + } + out_fn->gas_schedule_id = schedule_id_str; + schedule_id_str = NULL; + + JS_FreeValue(ctx, base); + JS_FreeValue(ctx, k_arg); + JS_FreeValue(ctx, k_ret); + JS_FreeValue(ctx, k_units); + JS_FreeValue(ctx, schedule_id); + + limits = JS_GetPropertyStr(ctx, fn_val, "limits"); + if (JS_IsException(limits)) + goto done; + if (js_host_validate_limits(ctx, limits, "limits", out_fn)) + goto done; + + error_codes = JS_GetPropertyStr(ctx, fn_val, "error_codes"); + if (JS_IsException(error_codes)) + goto done; + if (js_host_validate_error_codes(ctx, error_codes, "error_codes", out_fn)) + goto done; + + /* Gas overflow guard: base + (req * k_arg) + (resp * k_ret) + (units * k_units) */ + { + uint64_t total = out_fn->gas_base; + uint64_t arg_part = (uint64_t)out_fn->gas_k_arg_bytes * (uint64_t)out_fn->max_request_bytes; + uint64_t ret_part = (uint64_t)out_fn->gas_k_ret_bytes * (uint64_t)out_fn->max_response_bytes; + uint64_t unit_part = (uint64_t)out_fn->gas_k_units * (uint64_t)out_fn->max_units; + + if (arg_part > UINT64_MAX - total || + ret_part > UINT64_MAX - total - arg_part || + unit_part > UINT64_MAX - total - arg_part - ret_part) { + js_host_manifest_error(ctx, "gas", "gas charges overflow uint64 bounds"); + goto done; + } + } + + if (js_host_build_function_name(ctx, out_fn)) + goto done; + + ret = 0; + +done: + if (!JS_IsUndefined(fn_id)) + JS_FreeValue(ctx, fn_id); + if (!JS_IsUndefined(js_path)) + JS_FreeValue(ctx, js_path); + if (!JS_IsUndefined(effect)) + JS_FreeValue(ctx, effect); + if (!JS_IsUndefined(arity)) + JS_FreeValue(ctx, arity); + if (!JS_IsUndefined(arg_schema)) + JS_FreeValue(ctx, arg_schema); + if (!JS_IsUndefined(return_schema)) + JS_FreeValue(ctx, return_schema); + if (!JS_IsUndefined(gas)) + JS_FreeValue(ctx, gas); + if (!JS_IsUndefined(limits)) + JS_FreeValue(ctx, limits); + if (!JS_IsUndefined(error_codes)) + JS_FreeValue(ctx, error_codes); + if (eff_str) + JS_FreeCString(ctx, eff_str); + if (schedule_id_str) + js_free(ctx, schedule_id_str); + + if (ret != 0) { + if (out_fn->name) { + js_free(ctx, out_fn->name); + out_fn->name = NULL; + } + } + return ret; +} + +static BOOL js_host_paths_conflict(const JSHostFunctionDef *a, const JSHostFunctionDef *b) +{ + size_t len = a->path_len < b->path_len ? a->path_len : b->path_len; + for (size_t i = 0; i < len; i++) { + if (strcmp(a->path_segments[i], b->path_segments[i]) != 0) + return FALSE; + } + return TRUE; +} + +static int js_host_validate_manifest(JSContext *ctx, JSValueConst manifest_val, JSHostManifest *out_manifest) +{ + const char *required[] = {"abi_id", "abi_version", "functions"}; + JSValue abi_id = JS_UNDEFINED; + JSValue abi_version = JS_UNDEFINED; + JSValue functions = JS_UNDEFINED; + char *abi_id_str = NULL; + int ret = -1; + + if (js_host_expect_object(ctx, manifest_val, "manifest")) + return -1; + + if (js_host_check_keys(ctx, manifest_val, required, 3, NULL, 0, "manifest")) + return -1; + + abi_id = JS_GetPropertyStr(ctx, manifest_val, "abi_id"); + if (JS_IsException(abi_id)) + goto done; + if (js_host_copy_non_empty_string(ctx, abi_id, "manifest.abi_id", &abi_id_str)) + goto done; + if (strcmp(abi_id_str, "Host.v1") != 0) { + js_host_manifest_error(ctx, "manifest.abi_id", "unsupported abi_id (expected Host.v1)"); + goto done; + } + + abi_version = JS_GetPropertyStr(ctx, manifest_val, "abi_version"); + if (JS_IsException(abi_version)) + goto done; + uint32_t version = 0; + if (js_host_validate_uint32(ctx, abi_version, "manifest.abi_version", 1, UINT32_MAX, &version)) + goto done; + if (version != 1) { + js_host_manifest_error(ctx, "manifest.abi_version", "unsupported abi_version (expected 1)"); + goto done; + } + + functions = JS_GetPropertyStr(ctx, manifest_val, "functions"); + if (JS_IsException(functions)) + goto done; + if (!JS_IsArray(ctx, functions)) { + js_host_manifest_error(ctx, "manifest.functions", "functions must be an array"); + goto done; + } + + if (js_host_array_length(ctx, functions, &out_manifest->function_count)) + goto done; + if (out_manifest->function_count == 0) { + js_host_manifest_error(ctx, "manifest.functions", "functions must contain at least one entry"); + goto done; + } + + out_manifest->functions = js_malloc(ctx, sizeof(JSHostFunctionDef) * out_manifest->function_count); + if (!out_manifest->functions) + goto done; + memset(out_manifest->functions, 0, sizeof(JSHostFunctionDef) * out_manifest->function_count); + + uint32_t prev_fn_id = 0; + for (size_t i = 0; i < out_manifest->function_count; i++) { + JSValue fn_val = JS_GetPropertyUint32(ctx, functions, (uint32_t)i); + if (JS_IsException(fn_val)) + goto done; + + char fn_path[96]; + snprintf(fn_path, sizeof(fn_path), "manifest.functions[%zu]", i); + + if (js_host_validate_function(ctx, fn_val, fn_path, &out_manifest->functions[i])) { + JS_FreeValue(ctx, fn_val); + goto done; + } + + JS_FreeValue(ctx, fn_val); + + if (i > 0 && out_manifest->functions[i].fn_id <= prev_fn_id) { + js_host_manifest_error(ctx, "manifest.functions", "functions must be sorted by ascending fn_id"); + goto done; + } + prev_fn_id = out_manifest->functions[i].fn_id; + } + + for (size_t i = 0; i < out_manifest->function_count; i++) { + for (size_t j = i + 1; j < out_manifest->function_count; j++) { + if (js_host_paths_conflict(&out_manifest->functions[i], &out_manifest->functions[j])) { + js_host_manifest_error(ctx, "manifest.functions", "js_path collision detected"); + goto done; + } + } + } + + ret = 0; + +done: + if (abi_id_str) + js_free(ctx, abi_id_str); + if (!JS_IsUndefined(abi_id)) + JS_FreeValue(ctx, abi_id); + if (!JS_IsUndefined(abi_version)) + JS_FreeValue(ctx, abi_version); + if (!JS_IsUndefined(functions)) + JS_FreeValue(ctx, functions); + + if (ret != 0) + js_host_manifest_clear(ctx, out_manifest); + return ret; +} + +static int js_host_namespace_push(JSContext *ctx, + JSValue ns, + JSValue **out, + size_t *count, + size_t *capacity) +{ + void *ptr = JS_VALUE_GET_PTR(ns); + for (size_t i = 0; i < *count; i++) { + if (JS_VALUE_GET_PTR((*out)[i]) == ptr) + return 0; + } + + if (*count >= *capacity) { + size_t new_cap = (*capacity == 0) ? 8 : (*capacity * 2); + JSValue *resized = js_realloc(ctx, *out, sizeof(JSValue) * new_cap); + if (!resized) + return -1; + *out = resized; + *capacity = new_cap; + } + + (*out)[*count] = JS_DupValue(ctx, ns); + (*count)++; + return 0; +} + +static int js_host_get_or_create_child(JSContext *ctx, + JSValue parent, + const char *name, + JSValue *out_child, + JSValue **namespaces, + size_t *ns_count, + size_t *ns_capacity) +{ + JSValue child = JS_UNDEFINED; + JSAtom atom = JS_NewAtom(ctx, name); + int ret = -1; + + if (atom == JS_ATOM_NULL) + return -1; + + child = JS_GetProperty(ctx, parent, atom); + if (JS_IsException(child)) + goto done; + + if (JS_IsUndefined(child)) { + child = JS_NewObjectProto(ctx, JS_NULL); + if (JS_IsException(child)) + goto done; + if (JS_DefinePropertyValue(ctx, + parent, + atom, + JS_DupValue(ctx, child), + JS_PROP_HAS_VALUE | JS_PROP_HAS_CONFIGURABLE | + JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE) < 0) + goto done; + } else if (!JS_IsObject(child)) { + js_host_manifest_error(ctx, name, "Host namespace collision"); + goto done; + } + + if (js_host_namespace_push(ctx, child, namespaces, ns_count, ns_capacity)) + goto done; + + *out_child = child; + child = JS_UNDEFINED; + ret = 0; + +done: + if (atom != JS_ATOM_NULL) + JS_FreeAtom(ctx, atom); + if (!JS_IsUndefined(child)) + JS_FreeValue(ctx, child); + return ret; +} + +static int js_host_charge_pre(JSContext *ctx, const JSHostFunctionDef *fn, size_t req_len) +{ + uint64_t charge = fn->gas_base; + uint64_t arg_part = (uint64_t)fn->gas_k_arg_bytes * (uint64_t)req_len; + + if (arg_part > UINT64_MAX - charge) { + JS_ThrowTypeError(ctx, "host_call gas overflow"); + return -1; + } + + charge += arg_part; + return JS_UseGas(ctx, charge); +} + +static int js_host_charge_post(JSContext *ctx, const JSHostFunctionDef *fn, size_t resp_len, uint32_t units) +{ + uint64_t charge = 0; + uint64_t resp_part = (uint64_t)fn->gas_k_ret_bytes * (uint64_t)resp_len; + uint64_t unit_part = (uint64_t)fn->gas_k_units * (uint64_t)units; + + if (resp_part > UINT64_MAX - charge || unit_part > UINT64_MAX - charge - resp_part) { + JS_ThrowTypeError(ctx, "host_call gas overflow"); + return -1; + } + + charge += resp_part + unit_part; + return JS_UseGas(ctx, charge); +} + +static JSValue js_host_call_wrapper(JSContext *ctx, + JSValueConst this_val, + int argc, + JSValueConst *argv, + int magic) +{ + JSHostManifest *manifest = js_host_find_manifest(ctx); + JSDvBuffer req_buf = {0}; + JSValue args_array = JS_UNDEFINED; + JSHostResponse resp; + JSHostCallResult result = {0}; + JSValue ret = JS_EXCEPTION; + + if (!manifest) { + JS_ThrowTypeError(ctx, "host manifest is not initialized"); + return JS_EXCEPTION; + } + + if (magic < 0 || (size_t)magic >= manifest->function_count) { + JS_ThrowTypeError(ctx, "invalid host function binding"); + return JS_EXCEPTION; + } + + const JSHostFunctionDef *fn = &manifest->functions[magic]; + + if (argc != (int)fn->arity) { + JS_ThrowTypeError(ctx, "%s expects %u arguments", fn->name ? fn->name : "Host function", fn->arity); + return JS_EXCEPTION; + } + + for (size_t i = 0; i < fn->arity; i++) { + const JSHostArgDef *arg = &fn->args[i]; + JSValueConst val = argv[i]; + + switch (arg->type) { + case JS_HOST_SCHEMA_STRING: { + if (!JS_IsString(val)) { + JS_ThrowTypeError(ctx, "%s argument %zu must be a string", fn->name ? fn->name : "Host function", i + 1); + return JS_EXCEPTION; + } + if (arg->utf8_max > 0) { + size_t utf8_len = 0; + const char *str = JS_ToCStringLen2(ctx, &utf8_len, val, 0); + if (!str) + return JS_EXCEPTION; + JS_FreeCString(ctx, str); + if (utf8_len > arg->utf8_max) { + JS_ThrowTypeError(ctx, + "%s argument %zu exceeds utf8 limit (%zu > %u)", + fn->name ? fn->name : "Host function", + i + 1, + utf8_len, + arg->utf8_max); + return JS_EXCEPTION; + } + } + break; + } + case JS_HOST_SCHEMA_DV: + break; + case JS_HOST_SCHEMA_NULL: + if (!JS_IsNull(val)) { + JS_ThrowTypeError(ctx, "%s argument %zu must be null", fn->name ? fn->name : "Host function", i + 1); + return JS_EXCEPTION; + } + break; + } + } + + if (JS_RunGCCheckpoint(ctx)) + return JS_EXCEPTION; + + args_array = JS_NewArray(ctx); + if (JS_IsException(args_array)) + return JS_EXCEPTION; + + for (size_t i = 0; i < fn->arity; i++) { + if (JS_SetPropertyUint32(ctx, args_array, (uint32_t)i, JS_DupValue(ctx, argv[i])) < 0) { + JS_FreeValue(ctx, args_array); + return JS_EXCEPTION; + } + } + + JSDvLimits dv_limits = JS_DV_LIMIT_DEFAULTS; + dv_limits.max_encoded_bytes = fn->max_request_bytes; + + if (JS_EncodeDV(ctx, args_array, &dv_limits, &req_buf)) { + JS_FreeValue(ctx, args_array); + JS_FreeDVBuffer(ctx, &req_buf); + return JS_EXCEPTION; + } + + JS_FreeValue(ctx, args_array); + args_array = JS_UNDEFINED; + + if (js_host_charge_pre(ctx, fn, req_buf.length)) { + JS_FreeDVBuffer(ctx, &req_buf); + return JS_EXCEPTION; + } + + if (JS_RunGCCheckpoint(ctx)) { + JS_FreeDVBuffer(ctx, &req_buf); + return JS_EXCEPTION; + } + + if (JS_HostCall(ctx, + fn->fn_id, + req_buf.data, + req_buf.length, + fn->max_request_bytes, + fn->max_response_bytes, + &result)) { + JS_FreeDVBuffer(ctx, &req_buf); + return JS_EXCEPTION; + } + + JS_FreeDVBuffer(ctx, &req_buf); + + JSHostResponseValidation validation = { + .max_units = fn->max_units, + .errors = fn->errors, + .error_count = fn->error_count, + }; + + if (JS_ParseHostResponse(ctx, result.data, result.length, &validation, &resp)) + return JS_EXCEPTION; + + if (js_host_charge_post(ctx, fn, result.length, resp.units)) { + JS_FreeHostResponse(ctx, &resp); + return JS_EXCEPTION; + } + + if (JS_RunGCCheckpoint(ctx)) { + JS_FreeHostResponse(ctx, &resp); + return JS_EXCEPTION; + } + + if (resp.is_error) { + ret = JS_ThrowHostError(ctx, resp.err_code_atom, resp.err_tag_atom, resp.err_details); + JS_FreeHostResponse(ctx, &resp); + return ret; + } + + switch (fn->return_type) { + case JS_HOST_SCHEMA_STRING: + if (!JS_IsString(resp.ok)) { + JS_FreeHostResponse(ctx, &resp); + return js_throw_host_envelope_invalid(ctx); + } + break; + case JS_HOST_SCHEMA_NULL: + if (!JS_IsNull(resp.ok)) { + JS_FreeHostResponse(ctx, &resp); + return js_throw_host_envelope_invalid(ctx); + } + break; + case JS_HOST_SCHEMA_DV: + break; + } + + ret = resp.ok; + resp.ok = JS_UNDEFINED; + JS_FreeHostResponse(ctx, &resp); + return ret; +} + +static int js_host_install_functions(JSContext *ctx, JSHostManifest *manifest) +{ + JSValue global = JS_UNDEFINED; + JSValue host = JS_UNDEFINED; + JSValue host_v1 = JS_UNDEFINED; + JSValue *namespaces = NULL; + size_t ns_count = 0; + size_t ns_capacity = 0; + int ret = -1; + + global = JS_GetGlobalObject(ctx); + if (JS_IsException(global)) + goto done; + + if (js_host_get_or_create_child(ctx, global, "Host", &host, &namespaces, &ns_count, &ns_capacity)) + goto done; + + if (js_host_get_or_create_child(ctx, host, "v1", &host_v1, &namespaces, &ns_count, &ns_capacity)) + goto done; + + for (size_t i = 0; i < manifest->function_count; i++) { + JSValue current = JS_DupValue(ctx, host_v1); + if (JS_IsException(current)) + goto done; + + JSValue next = JS_UNDEFINED; + JSHostFunctionDef *fn = &manifest->functions[i]; + + for (size_t j = 0; j + 1 < fn->path_len; j++) { + if (js_host_get_or_create_child(ctx, + current, + fn->path_segments[j], + &next, + &namespaces, + &ns_count, + &ns_capacity)) { + JS_FreeValue(ctx, current); + goto done; + } + JS_FreeValue(ctx, current); + current = next; + next = JS_UNDEFINED; + } + + JSAtom fn_atom = JS_NewAtom(ctx, fn->path_segments[fn->path_len - 1]); + if (fn_atom == JS_ATOM_NULL) { + JS_FreeValue(ctx, current); + goto done; + } + + JSValue cfunc = JS_NewCFunctionMagic(ctx, + js_host_call_wrapper, + fn->path_segments[fn->path_len - 1], + fn->arity, + JS_CFUNC_generic_magic, + (int)i); + if (JS_IsException(cfunc)) { + JS_FreeAtom(ctx, fn_atom); + JS_FreeValue(ctx, current); + goto done; + } + + if (JS_DefinePropertyValue(ctx, + current, + fn_atom, + cfunc, + JS_PROP_HAS_VALUE | JS_PROP_HAS_CONFIGURABLE | + JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE) < 0) { + JS_FreeAtom(ctx, fn_atom); + JS_FreeValue(ctx, current); + goto done; + } + + JS_FreeAtom(ctx, fn_atom); + JS_FreeValue(ctx, current); + } + + for (size_t i = 0; i < ns_count; i++) { + if (JS_PreventExtensions(ctx, namespaces[i]) < 0) + goto done; + } + + ret = 0; + +done: + if (namespaces) { + for (size_t i = 0; i < ns_count; i++) + JS_FreeValue(ctx, namespaces[i]); + js_free(ctx, namespaces); + } + if (!JS_IsUndefined(host_v1)) + JS_FreeValue(ctx, host_v1); + if (!JS_IsUndefined(host)) + JS_FreeValue(ctx, host); + if (!JS_IsUndefined(global)) + JS_FreeValue(ctx, global); + return ret; +} + +int JS_InitHostFromManifest(JSContext *ctx, const uint8_t *manifest_bytes, size_t manifest_size) +{ + JSValue manifest_val = JS_UNDEFINED; + JSHostManifest manifest = {0}; + JSHostManifestNode *node = NULL; + int ret = -1; + + if (!ctx || !manifest_bytes || manifest_size == 0) { + JS_ThrowTypeError(ctx, "abi manifest is required"); + return -1; + } + + if (js_host_find_manifest(ctx)) { + JS_ThrowTypeError(ctx, "abi manifest is already initialized"); + return -1; + } + + manifest_val = JS_DecodeDV(ctx, manifest_bytes, manifest_size, &JS_DV_LIMIT_DEFAULTS); + if (JS_IsException(manifest_val)) + return -1; + + if (js_host_validate_manifest(ctx, manifest_val, &manifest)) + goto done; + + node = js_malloc(ctx, sizeof(JSHostManifestNode)); + if (!node) + goto done; + + node->ctx = ctx; + node->manifest = manifest; + node->next = NULL; + memset(&manifest, 0, sizeof(manifest)); /* ownership moved */ + + if (js_host_install_functions(ctx, &node->manifest)) + goto done; + + node->next = js_host_manifest_list; + js_host_manifest_list = node; + node = NULL; + + ret = 0; + +done: + if (!JS_IsUndefined(manifest_val)) + JS_FreeValue(ctx, manifest_val); + if (node) { + js_host_manifest_clear(ctx, &node->manifest); + js_free(ctx, node); + } else if (ret != 0) { + js_host_manifest_clear(ctx, &manifest); + } + return ret; +} diff --git a/quickjs-host.h b/quickjs-host.h index 3ab2cbe10..1f51f5954 100644 --- a/quickjs-host.h +++ b/quickjs-host.h @@ -23,6 +23,8 @@ typedef struct JSHostResponse { JSValue err_details; } JSHostResponse; +typedef struct JSHostManifest JSHostManifest; + /* Host response envelope helpers (T-039). */ JSValue JS_ThrowHostError(JSContext *ctx, JSAtom code_atom, JSAtom tag_atom, JSValueConst details); JSValue JS_ThrowHostTransportError(JSContext *ctx); @@ -32,5 +34,7 @@ int JS_ParseHostResponse(JSContext *ctx, const JSHostResponseValidation *validation, JSHostResponse *out); void JS_FreeHostResponse(JSContext *ctx, JSHostResponse *resp); +int JS_InitHostFromManifest(JSContext *ctx, const uint8_t *manifest_bytes, size_t manifest_size); +void JS_FreeHostManifest(JSContext *ctx); #endif /* QUICKJS_HOST_H */ diff --git a/quickjs.c b/quickjs.c index 3cc7d35b9..8e441310c 100644 --- a/quickjs.c +++ b/quickjs.c @@ -2889,9 +2889,6 @@ static int js_deterministic_init_host(JSContext *ctx) if (ret < 0) goto fail; - if (JS_PreventExtensions(ctx, host_v1) < 0 || JS_PreventExtensions(ctx, host_ns) < 0) - goto fail; - JS_FreeValue(ctx, host_ns); JS_FreeValue(ctx, host_v1); @@ -3127,6 +3124,13 @@ int JS_InitDeterministicContext(JSContext *ctx, const JSDeterministicInitOptions memcpy(context_copy, options->context_blob, options->context_blob_size); } + if (JS_InitHostFromManifest(ctx, manifest_copy, options->manifest_size) != 0) { + js_free_rt(ctx->rt, manifest_copy); + if (context_copy) + js_free_rt(ctx->rt, context_copy); + return -1; + } + memcpy(ctx->abi_manifest_hash, computed_hash, sizeof(computed_hash)); js_sha256_to_hex(computed_hash, ctx->abi_manifest_hash_hex); ctx->abi_manifest_bytes = manifest_copy; @@ -3549,6 +3553,7 @@ void JS_FreeContext(JSContext *ctx) } #endif + JS_FreeHostManifest(ctx); js_free_modules(ctx, JS_FREE_MODULE_ALL); JS_FreeValue(ctx, ctx->global_obj); From 9b3dbfd8d4208109033c0babadb4f91ed594e227 Mon Sep 17 00:00:00 2001 From: Mateusz Jonak Date: Tue, 16 Dec 2025 23:26:42 +0100 Subject: [PATCH 18/19] feat(ergonomics): add document/canon/event globals from Host.v1 + context blob (T-041) - add JS_InitErgonomicGlobals() to install: - global document() wrapper + document.canonical() backed by Host.v1.document.{get,getCanonical} - global canon.{unwrap,at} for canonical DV clone+freeze and safe path lookup - global event/eventCanonical/steps decoded from context blob (deep-frozen) - implement deep freeze utilities and canonical clone via DV roundtrip - wire ergonomic globals init into JS_InitDeterministicContext when a context blob is provided Refs: T-041 --- quickjs-host.c | 605 +++++++++++++++++++++++++++++++++++++++++++++++++ quickjs-host.h | 1 + quickjs.c | 9 + 3 files changed, 615 insertions(+) diff --git a/quickjs-host.c b/quickjs-host.c index 0deeaa91b..528e9c340 100644 --- a/quickjs-host.c +++ b/quickjs-host.c @@ -1818,3 +1818,608 @@ int JS_InitHostFromManifest(JSContext *ctx, const uint8_t *manifest_bytes, size_ } return ret; } + +/* Ergonomic globals (T-041) */ + +static int js_freeze_value(JSContext *ctx, JSValueConst val) +{ + JSPropertyEnum *props = NULL; + uint32_t props_len = 0; + int ret = -1; + + if (!JS_IsObject(val)) + return 0; + + if (JS_GetOwnPropertyNames(ctx, &props, &props_len, val, + JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY) < 0) + return -1; + + for (uint32_t i = 0; i < props_len; i++) { + JSAtom atom = props[i].atom; + JSValue prop_val = JS_GetProperty(ctx, val, atom); + int flags; + + if (JS_IsException(prop_val)) + goto done; + + if (js_freeze_value(ctx, prop_val)) { + JS_FreeValue(ctx, prop_val); + goto done; + } + + flags = JS_PROP_HAS_VALUE | JS_PROP_HAS_WRITABLE | JS_PROP_HAS_CONFIGURABLE | JS_PROP_HAS_ENUMERABLE; + if (props[i].is_enumerable) + flags |= JS_PROP_ENUMERABLE; + + if (JS_DefinePropertyValue(ctx, val, atom, JS_DupValue(ctx, prop_val), flags) < 0) { + JS_FreeValue(ctx, prop_val); + goto done; + } + + JS_FreeValue(ctx, prop_val); + } + + if (JS_IsArray(ctx, val)) { + JSAtom length_atom = JS_NewAtom(ctx, "length"); + JSValue length_val = JS_UNDEFINED; + + if (length_atom == JS_ATOM_NULL) + goto done; + + length_val = JS_GetProperty(ctx, val, length_atom); + if (JS_IsException(length_val)) { + JS_FreeAtom(ctx, length_atom); + goto done; + } + + if (JS_DefinePropertyValue(ctx, + val, + length_atom, + JS_DupValue(ctx, length_val), + JS_PROP_HAS_VALUE | JS_PROP_HAS_WRITABLE | JS_PROP_HAS_CONFIGURABLE | + JS_PROP_HAS_ENUMERABLE) < 0) { + JS_FreeAtom(ctx, length_atom); + JS_FreeValue(ctx, length_val); + goto done; + } + + JS_FreeAtom(ctx, length_atom); + JS_FreeValue(ctx, length_val); + } + + if (JS_PreventExtensions(ctx, val) < 0) + goto done; + + ret = 0; + +done: + if (props) + JS_FreePropertyEnum(ctx, props, props_len); + return ret; +} + +static int js_canon_clone_and_freeze(JSContext *ctx, JSValueConst input, JSValue *out) +{ + JSDvBuffer buf = {0}; + JSDvLimits limits = JS_DV_LIMIT_DEFAULTS; + JSValue decoded = JS_UNDEFINED; + + if (!out) + return -1; + + if (JS_RunGCCheckpoint(ctx)) + return -1; + + if (JS_EncodeDV(ctx, input, &limits, &buf)) + return -1; + + decoded = JS_DecodeDV(ctx, buf.data, buf.length, &limits); + JS_FreeDVBuffer(ctx, &buf); + if (JS_IsException(decoded)) + return -1; + + if (js_freeze_value(ctx, decoded)) { + JS_FreeValue(ctx, decoded); + return -1; + } + + *out = decoded; + return 0; +} + +static int js_context_copy_and_freeze(JSContext *ctx, + JSValueConst source, + JSAtom atom, + JSValue *out) +{ + JSValue tmp = JS_UNDEFINED; + JSValue dup = JS_UNDEFINED; + + if (!out) + return -1; + + tmp = JS_GetProperty(ctx, source, atom); + if (JS_IsException(tmp)) + return -1; + + if (!JS_IsUndefined(tmp)) { + dup = JS_DupValue(ctx, tmp); + if (js_freeze_value(ctx, dup)) { + JS_FreeValue(ctx, dup); + JS_FreeValue(ctx, tmp); + return -1; + } + *out = dup; + } + + JS_FreeValue(ctx, tmp); + return 0; +} + +static int js_decode_context_blob(JSContext *ctx, + const uint8_t *context_blob, + size_t context_blob_size, + JSValue *out_event, + JSValue *out_event_canonical, + JSValue *out_steps) +{ + JSValue decoded = JS_UNDEFINED; + JSAtom event_atom = JS_ATOM_NULL; + JSAtom event_canonical_atom = JS_ATOM_NULL; + JSAtom steps_atom = JS_ATOM_NULL; + int ret = -1; + + if (!out_event || !out_event_canonical || !out_steps) + return -1; + + *out_event = JS_NULL; + *out_event_canonical = JS_NULL; + *out_steps = JS_NULL; + + if (!context_blob || context_blob_size == 0) + return 0; + + decoded = JS_DecodeDV(ctx, context_blob, context_blob_size, &JS_DV_LIMIT_DEFAULTS); + if (JS_IsException(decoded)) + return -1; + + if (!JS_IsObject(decoded) || JS_IsArray(ctx, decoded)) { + JS_FreeValue(ctx, decoded); + JS_ThrowTypeError(ctx, "context blob must decode to an object"); + return -1; + } + + event_atom = JS_NewAtom(ctx, "event"); + event_canonical_atom = JS_NewAtom(ctx, "eventCanonical"); + steps_atom = JS_NewAtom(ctx, "steps"); + if (event_atom == JS_ATOM_NULL || event_canonical_atom == JS_ATOM_NULL || steps_atom == JS_ATOM_NULL) + goto done; + + if (js_context_copy_and_freeze(ctx, decoded, event_atom, out_event)) + goto done; + if (js_context_copy_and_freeze(ctx, decoded, event_canonical_atom, out_event_canonical)) + goto done; + if (js_context_copy_and_freeze(ctx, decoded, steps_atom, out_steps)) + goto done; + + ret = 0; + +done: + if (event_atom != JS_ATOM_NULL) + JS_FreeAtom(ctx, event_atom); + if (event_canonical_atom != JS_ATOM_NULL) + JS_FreeAtom(ctx, event_canonical_atom); + if (steps_atom != JS_ATOM_NULL) + JS_FreeAtom(ctx, steps_atom); + if (!JS_IsUndefined(decoded)) + JS_FreeValue(ctx, decoded); + return ret; +} + +static JSValue js_document_wrapper(JSContext *ctx, + JSValueConst this_val, + int argc, + JSValueConst *argv, + int magic, + JSValue *func_data) +{ + (void)this_val; + (void)magic; + + if (!func_data || !JS_IsFunction(ctx, func_data[0])) { + JS_ThrowTypeError(ctx, "Host.v1.document binding is missing"); + return JS_EXCEPTION; + } + + return JS_Call(ctx, func_data[0], JS_UNDEFINED, argc, argv); +} + +static JSValue js_canon_unwrap(JSContext *ctx, + JSValueConst this_val, + int argc, + JSValueConst *argv) +{ + JSValue clone = JS_UNDEFINED; + + (void)this_val; + + if (argc != 1) { + JS_ThrowTypeError(ctx, "canon.unwrap expects 1 argument"); + return JS_EXCEPTION; + } + + if (js_canon_clone_and_freeze(ctx, argv[0], &clone)) + return JS_EXCEPTION; + + return clone; +} + +static JSValue js_canon_at(JSContext *ctx, + JSValueConst this_val, + int argc, + JSValueConst *argv) +{ + JSValue canonical = JS_UNDEFINED; + JSValue current = JS_UNDEFINED; + JSValue ret = JS_UNDEFINED; + uint32_t path_len = 0; + BOOL missing = FALSE; + + (void)this_val; + + if (argc < 2) { + JS_ThrowTypeError(ctx, "canon.at expects a value and a path array"); + return JS_EXCEPTION; + } + + if (!JS_IsArray(ctx, argv[1])) { + JS_ThrowTypeError(ctx, "canon.at path must be an array"); + return JS_EXCEPTION; + } + + if (js_canon_clone_and_freeze(ctx, argv[0], &canonical)) + return JS_EXCEPTION; + + { + JSValue len_val = JS_GetPropertyStr(ctx, argv[1], "length"); + if (JS_IsException(len_val)) { + JS_FreeValue(ctx, canonical); + return JS_EXCEPTION; + } + if (JS_ToUint32(ctx, &path_len, len_val)) { + JS_FreeValue(ctx, len_val); + JS_FreeValue(ctx, canonical); + return JS_EXCEPTION; + } + JS_FreeValue(ctx, len_val); + } + + current = JS_DupValue(ctx, canonical); + + for (uint32_t i = 0; i < path_len; i++) { + JSValue segment = JS_GetPropertyUint32(ctx, argv[1], i); + + if (JS_IsException(segment)) { + JS_FreeValue(ctx, current); + JS_FreeValue(ctx, canonical); + return JS_EXCEPTION; + } + + if (!JS_IsObject(current)) { + JS_FreeValue(ctx, segment); + missing = TRUE; + break; + } + + if (JS_IsString(segment)) { + size_t utf8_len = 0; + const char *prop = JS_ToCStringLen2(ctx, &utf8_len, segment, 0); + JSValue next = JS_UNDEFINED; + + if (!prop) { + JS_FreeValue(ctx, segment); + JS_FreeValue(ctx, current); + JS_FreeValue(ctx, canonical); + return JS_EXCEPTION; + } + + if (utf8_len > JS_DV_LIMIT_DEFAULTS.max_string_bytes) { + JS_FreeCString(ctx, prop); + JS_FreeValue(ctx, segment); + JS_FreeValue(ctx, current); + JS_FreeValue(ctx, canonical); + JS_ThrowTypeError(ctx, "canon.at path segment exceeds string limit"); + return JS_EXCEPTION; + } + + next = JS_GetPropertyStr(ctx, current, prop); + JS_FreeCString(ctx, prop); + JS_FreeValue(ctx, segment); + + if (JS_IsException(next)) { + JS_FreeValue(ctx, current); + JS_FreeValue(ctx, canonical); + return JS_EXCEPTION; + } + + if (JS_IsUndefined(next)) { + JS_FreeValue(ctx, next); + missing = TRUE; + break; + } + + JS_FreeValue(ctx, current); + current = next; + } else { + BOOL is_number = JS_IsNumber(segment); + BOOL is_bigint = JS_IsBigInt(ctx, segment); + double idx_d = 0; + int64_t index = 0; + JSValue next = JS_UNDEFINED; + + if (!is_number && !is_bigint) { + JS_FreeValue(ctx, segment); + JS_FreeValue(ctx, current); + JS_FreeValue(ctx, canonical); + JS_ThrowTypeError(ctx, "canon.at path elements must be strings or integers"); + return JS_EXCEPTION; + } + + if (is_number) { + if (JS_ToFloat64(ctx, &idx_d, segment)) { + JS_FreeValue(ctx, segment); + JS_FreeValue(ctx, current); + JS_FreeValue(ctx, canonical); + return JS_EXCEPTION; + } + + if (!isfinite(idx_d) || floor(idx_d) != idx_d || (idx_d == 0.0 && signbit(idx_d))) { + JS_FreeValue(ctx, segment); + JS_FreeValue(ctx, current); + JS_FreeValue(ctx, canonical); + JS_ThrowTypeError(ctx, "canon.at path elements must be strings or integers"); + return JS_EXCEPTION; + } + + index = (int64_t)idx_d; + } else { + if (JS_ToInt64Ext(ctx, &index, segment)) { + JS_FreeValue(ctx, segment); + JS_FreeValue(ctx, current); + JS_FreeValue(ctx, canonical); + JS_ThrowTypeError(ctx, "canon.at path elements must be strings or integers"); + return JS_EXCEPTION; + } + } + + JS_FreeValue(ctx, segment); + + if (index < 0 || (uint64_t)index >= JS_DV_LIMIT_DEFAULTS.max_array_length) { + JS_FreeValue(ctx, current); + JS_FreeValue(ctx, canonical); + JS_ThrowTypeError(ctx, "canon.at path index is out of range"); + return JS_EXCEPTION; + } + + if (!JS_IsArray(ctx, current)) { + missing = TRUE; + break; + } + + next = JS_GetPropertyUint32(ctx, current, (uint32_t)index); + if (JS_IsException(next)) { + JS_FreeValue(ctx, current); + JS_FreeValue(ctx, canonical); + return JS_EXCEPTION; + } + + if (JS_IsUndefined(next)) { + JS_FreeValue(ctx, next); + missing = TRUE; + break; + } + + JS_FreeValue(ctx, current); + current = next; + } + } + + if (missing) { + JS_FreeValue(ctx, current); + current = JS_UNDEFINED; + ret = JS_UNDEFINED; + } else { + ret = current; + current = JS_UNDEFINED; + } + + JS_FreeValue(ctx, canonical); + if (!JS_IsUndefined(current)) + JS_FreeValue(ctx, current); + return ret; +} + +int JS_InitErgonomicGlobals(JSContext *ctx, const uint8_t *context_blob, size_t context_blob_size) +{ + JSValue global = JS_UNDEFINED; + JSValue host = JS_UNDEFINED; + JSValue host_v1 = JS_UNDEFINED; + JSValue document_ns = JS_UNDEFINED; + JSValue document_get = JS_UNDEFINED; + JSValue document_get_canonical = JS_UNDEFINED; + JSValue document_fn = JS_UNDEFINED; + JSValue document_canonical_fn = JS_UNDEFINED; + JSValue canon_obj = JS_UNDEFINED; + JSValue canon_unwrap_fn = JS_UNDEFINED; + JSValue canon_at_fn = JS_UNDEFINED; + JSValue event_val = JS_NULL; + JSValue event_canonical_val = JS_NULL; + JSValue steps_val = JS_NULL; + JSValueConst doc_funcs[1]; + int ret = -1; + + if (!ctx) + return -1; + + if (!js_host_find_manifest(ctx)) { + JS_ThrowTypeError(ctx, "abi manifest must be initialized before installing ergonomic globals"); + return -1; + } + + if (js_decode_context_blob(ctx, context_blob, context_blob_size, &event_val, &event_canonical_val, &steps_val)) + goto done; + + global = JS_GetGlobalObject(ctx); + if (JS_IsException(global)) + goto done; + + host = JS_GetPropertyStr(ctx, global, "Host"); + if (JS_IsException(host)) + goto done; + + host_v1 = JS_GetPropertyStr(ctx, host, "v1"); + if (JS_IsException(host_v1)) + goto done; + + document_ns = JS_GetPropertyStr(ctx, host_v1, "document"); + if (JS_IsException(document_ns)) + goto done; + + document_get = JS_GetPropertyStr(ctx, document_ns, "get"); + if (JS_IsException(document_get)) + goto done; + + document_get_canonical = JS_GetPropertyStr(ctx, document_ns, "getCanonical"); + if (JS_IsException(document_get_canonical)) + goto done; + + if (!JS_IsFunction(ctx, document_get) || !JS_IsFunction(ctx, document_get_canonical)) { + JS_ThrowTypeError(ctx, "Host.v1.document bindings are missing"); + goto done; + } + + doc_funcs[0] = JS_DupValue(ctx, document_get); + document_fn = JS_NewCFunctionData(ctx, js_document_wrapper, 1, 0, 1, doc_funcs); + JS_FreeValue(ctx, (JSValue)doc_funcs[0]); + if (JS_IsException(document_fn)) + goto done; + + doc_funcs[0] = JS_DupValue(ctx, document_get_canonical); + document_canonical_fn = JS_NewCFunctionData(ctx, js_document_wrapper, 1, 0, 1, doc_funcs); + JS_FreeValue(ctx, (JSValue)doc_funcs[0]); + if (JS_IsException(document_canonical_fn)) + goto done; + + if (JS_DefinePropertyValueStr(ctx, + global, + "document", + JS_DupValue(ctx, document_fn), + JS_PROP_HAS_VALUE | JS_PROP_HAS_WRITABLE | JS_PROP_HAS_CONFIGURABLE) < 0) + goto done; + + if (JS_DefinePropertyValueStr(ctx, + document_fn, + "canonical", + JS_DupValue(ctx, document_canonical_fn), + JS_PROP_HAS_VALUE | JS_PROP_HAS_WRITABLE | JS_PROP_HAS_CONFIGURABLE) < 0) + goto done; + + if (JS_PreventExtensions(ctx, document_fn) < 0) + goto done; + if (JS_PreventExtensions(ctx, document_canonical_fn) < 0) + goto done; + + canon_obj = JS_NewObjectProto(ctx, JS_NULL); + if (JS_IsException(canon_obj)) + goto done; + + canon_unwrap_fn = JS_NewCFunction(ctx, js_canon_unwrap, "unwrap", 1); + if (JS_IsException(canon_unwrap_fn)) + goto done; + + canon_at_fn = JS_NewCFunction(ctx, js_canon_at, "at", 2); + if (JS_IsException(canon_at_fn)) + goto done; + + if (JS_DefinePropertyValueStr(ctx, + canon_obj, + "unwrap", + JS_DupValue(ctx, canon_unwrap_fn), + JS_PROP_HAS_VALUE | JS_PROP_HAS_WRITABLE | JS_PROP_HAS_CONFIGURABLE) < 0) + goto done; + + if (JS_DefinePropertyValueStr(ctx, + canon_obj, + "at", + JS_DupValue(ctx, canon_at_fn), + JS_PROP_HAS_VALUE | JS_PROP_HAS_WRITABLE | JS_PROP_HAS_CONFIGURABLE) < 0) + goto done; + + if (JS_PreventExtensions(ctx, canon_unwrap_fn) < 0) + goto done; + if (JS_PreventExtensions(ctx, canon_at_fn) < 0) + goto done; + if (JS_PreventExtensions(ctx, canon_obj) < 0) + goto done; + + if (JS_DefinePropertyValueStr(ctx, + global, + "canon", + JS_DupValue(ctx, canon_obj), + JS_PROP_HAS_VALUE | JS_PROP_HAS_WRITABLE | JS_PROP_HAS_CONFIGURABLE) < 0) + goto done; + + if (JS_DefinePropertyValueStr(ctx, + global, + "event", + JS_DupValue(ctx, event_val), + JS_PROP_HAS_VALUE | JS_PROP_HAS_WRITABLE | JS_PROP_HAS_CONFIGURABLE) < 0) + goto done; + + if (JS_DefinePropertyValueStr(ctx, + global, + "eventCanonical", + JS_DupValue(ctx, event_canonical_val), + JS_PROP_HAS_VALUE | JS_PROP_HAS_WRITABLE | JS_PROP_HAS_CONFIGURABLE) < 0) + goto done; + + if (JS_DefinePropertyValueStr(ctx, + global, + "steps", + JS_DupValue(ctx, steps_val), + JS_PROP_HAS_VALUE | JS_PROP_HAS_WRITABLE | JS_PROP_HAS_CONFIGURABLE) < 0) + goto done; + + ret = 0; + +done: + if (!JS_IsUndefined(host_v1)) + JS_FreeValue(ctx, host_v1); + if (!JS_IsUndefined(host)) + JS_FreeValue(ctx, host); + if (!JS_IsUndefined(global)) + JS_FreeValue(ctx, global); + if (!JS_IsUndefined(document_ns)) + JS_FreeValue(ctx, document_ns); + if (!JS_IsUndefined(document_get)) + JS_FreeValue(ctx, document_get); + if (!JS_IsUndefined(document_get_canonical)) + JS_FreeValue(ctx, document_get_canonical); + if (!JS_IsUndefined(document_fn)) + JS_FreeValue(ctx, document_fn); + if (!JS_IsUndefined(document_canonical_fn)) + JS_FreeValue(ctx, document_canonical_fn); + if (!JS_IsUndefined(canon_obj)) + JS_FreeValue(ctx, canon_obj); + if (!JS_IsUndefined(canon_unwrap_fn)) + JS_FreeValue(ctx, canon_unwrap_fn); + if (!JS_IsUndefined(canon_at_fn)) + JS_FreeValue(ctx, canon_at_fn); + if (!JS_IsUndefined(event_val)) + JS_FreeValue(ctx, event_val); + if (!JS_IsUndefined(event_canonical_val)) + JS_FreeValue(ctx, event_canonical_val); + if (!JS_IsUndefined(steps_val)) + JS_FreeValue(ctx, steps_val); + return ret; +} diff --git a/quickjs-host.h b/quickjs-host.h index 1f51f5954..7d1fcf4a0 100644 --- a/quickjs-host.h +++ b/quickjs-host.h @@ -35,6 +35,7 @@ int JS_ParseHostResponse(JSContext *ctx, JSHostResponse *out); void JS_FreeHostResponse(JSContext *ctx, JSHostResponse *resp); int JS_InitHostFromManifest(JSContext *ctx, const uint8_t *manifest_bytes, size_t manifest_size); +int JS_InitErgonomicGlobals(JSContext *ctx, const uint8_t *context_blob, size_t context_blob_size); void JS_FreeHostManifest(JSContext *ctx); #endif /* QUICKJS_HOST_H */ diff --git a/quickjs.c b/quickjs.c index 8e441310c..2d13d3401 100644 --- a/quickjs.c +++ b/quickjs.c @@ -3131,6 +3131,15 @@ int JS_InitDeterministicContext(JSContext *ctx, const JSDeterministicInitOptions return -1; } + if (context_copy && options->context_blob_size > 0) { + if (JS_InitErgonomicGlobals(ctx, context_copy, options->context_blob_size) != 0) { + JS_FreeHostManifest(ctx); + js_free_rt(ctx->rt, manifest_copy); + js_free_rt(ctx->rt, context_copy); + return -1; + } + } + memcpy(ctx->abi_manifest_hash, computed_hash, sizeof(computed_hash)); js_sha256_to_hex(computed_hash, ctx->abi_manifest_hash_hex); ctx->abi_manifest_bytes = manifest_copy; From e29f46513c0d4a198e8ee8a0fe090d476dcf19d5 Mon Sep 17 00:00:00 2001 From: Mateusz Jonak Date: Wed, 17 Dec 2025 09:46:26 +0100 Subject: [PATCH 19/19] feat(host-tape): add optional host-call tape ring buffer for tracing (T-043) - quickjs-host: store per-context tape state alongside the loaded host manifest - add tape API: JS_EnableHostTape / JS_ResetHostTape / JS_GetHostTapeLength / JS_ReadHostTape - record Host.v1 call metadata (fn_id, req/resp lengths, units, is_error, charge_failed) - hash request/response bytes (sha256) and record computed pre/post gas charges - ensure tape memory is freed with JS_FreeHostManifest Refs: T-043 --- quickjs-host.c | 231 ++++++++++++++++++++++++++++++++++++++++++++++++- quickjs-host.h | 21 +++++ 2 files changed, 250 insertions(+), 2 deletions(-) diff --git a/quickjs-host.c b/quickjs-host.c index 528e9c340..b46b69fcf 100644 --- a/quickjs-host.c +++ b/quickjs-host.c @@ -451,9 +451,17 @@ struct JSHostManifest { size_t function_count; }; +typedef struct { + JSHostTapeRecord *records; + size_t capacity; + size_t count; + size_t head; +} JSHostTapeState; + typedef struct JSHostManifestNode { JSContext *ctx; JSHostManifest manifest; + JSHostTapeState tape; struct JSHostManifestNode *next; } JSHostManifestNode; @@ -470,6 +478,25 @@ static JSHostManifest *js_host_find_manifest(JSContext *ctx) return NULL; } +static JSHostManifestNode *js_host_find_manifest_node(JSContext *ctx) +{ + JSHostManifestNode *node = js_host_manifest_list; + while (node) { + if (node->ctx == ctx) + return node; + node = node->next; + } + return NULL; +} + +static JSHostTapeState *js_host_get_tape(JSContext *ctx) +{ + JSHostManifestNode *node = js_host_find_manifest_node(ctx); + if (!node) + return NULL; + return &node->tape; +} + static void js_host_free_function(JSContext *ctx, JSHostFunctionDef *fn) { if (!fn) @@ -519,6 +546,67 @@ static void js_host_manifest_clear(JSContext *ctx, JSHostManifest *manifest) manifest->function_count = 0; } +static void js_host_tape_free(JSContext *ctx, JSHostTapeState *tape) +{ + if (!tape) + return; + + if (tape->records) + js_free(ctx, tape->records); + + tape->records = NULL; + tape->capacity = 0; + tape->count = 0; + tape->head = 0; +} + +static int js_host_calc_gas_charges(const JSHostFunctionDef *fn, + size_t req_len, + size_t resp_len, + uint32_t units, + uint64_t *out_pre, + uint64_t *out_post) +{ + uint64_t pre = 0; + uint64_t post = 0; + uint64_t arg_part = 0; + uint64_t resp_part = 0; + uint64_t unit_part = 0; + + if (!fn) + return -1; + + pre = fn->gas_base; + arg_part = (uint64_t)fn->gas_k_arg_bytes * (uint64_t)req_len; + if (arg_part > UINT64_MAX - pre) + return -1; + pre += arg_part; + + resp_part = (uint64_t)fn->gas_k_ret_bytes * (uint64_t)resp_len; + unit_part = (uint64_t)fn->gas_k_units * (uint64_t)units; + if (resp_part > UINT64_MAX - unit_part) + return -1; + post = resp_part + unit_part; + + if (out_pre) + *out_pre = pre; + if (out_post) + *out_post = post; + return 0; +} + +static void js_host_tape_append(JSHostTapeState *tape, const JSHostTapeRecord *record) +{ + if (!tape || !record || tape->capacity == 0 || !tape->records) + return; + + size_t idx = tape->head; + tape->records[idx] = *record; + if (tape->count < tape->capacity) + tape->count++; + tape->head = (tape->head + 1) % tape->capacity; +} + void JS_FreeHostManifest(JSContext *ctx) { JSHostManifestNode *prev = NULL; @@ -532,6 +620,7 @@ void JS_FreeHostManifest(JSContext *ctx) js_host_manifest_list = node->next; js_host_manifest_clear(ctx, &node->manifest); + js_host_tape_free(ctx, &node->tape); js_free(ctx, node); return; } @@ -540,6 +629,98 @@ void JS_FreeHostManifest(JSContext *ctx) } } +int JS_EnableHostTape(JSContext *ctx, size_t capacity) +{ + JSHostTapeState *tape = js_host_get_tape(ctx); + JSHostTapeRecord *records = NULL; + + if (!ctx) + return -1; + + if (!tape) { + JS_ThrowTypeError(ctx, "host manifest is not initialized"); + return -1; + } + + if (capacity > JS_HOST_TAPE_MAX_CAPACITY) { + JS_ThrowTypeError(ctx, "tape capacity exceeds max (%u)", JS_HOST_TAPE_MAX_CAPACITY); + return -1; + } + + if (capacity == 0) { + js_host_tape_free(ctx, tape); + return 0; + } + + records = js_mallocz(ctx, sizeof(JSHostTapeRecord) * capacity); + if (!records) + return -1; + + js_host_tape_free(ctx, tape); + tape->records = records; + tape->capacity = capacity; + tape->count = 0; + tape->head = 0; + return 0; +} + +int JS_ResetHostTape(JSContext *ctx) +{ + JSHostTapeState *tape = js_host_get_tape(ctx); + + if (!ctx) + return -1; + + if (!tape) { + JS_ThrowTypeError(ctx, "host manifest is not initialized"); + return -1; + } + + if (tape->records && tape->capacity > 0) + memset(tape->records, 0, sizeof(JSHostTapeRecord) * tape->capacity); + + tape->count = 0; + tape->head = 0; + return 0; +} + +size_t JS_GetHostTapeLength(JSContext *ctx) +{ + JSHostTapeState *tape = js_host_get_tape(ctx); + if (!tape) + return 0; + return tape->count; +} + +int JS_ReadHostTape(JSContext *ctx, JSHostTapeRecord *out_records, size_t max_records, size_t *out_count) +{ + JSHostTapeState *tape = js_host_get_tape(ctx); + size_t to_copy = 0; + + if (!ctx) + return -1; + + if (!tape) { + JS_ThrowTypeError(ctx, "host manifest is not initialized"); + return -1; + } + + if (out_count) + *out_count = tape->count; + + if (!out_records || max_records == 0 || tape->count == 0 || tape->capacity == 0 || !tape->records) + return 0; + + to_copy = tape->count < max_records ? tape->count : max_records; + size_t start = (tape->head + tape->capacity - tape->count) % tape->capacity; + for (size_t i = 0; i < to_copy; i++) { + size_t idx = (start + i) % tape->capacity; + out_records[i] = tape->records[idx]; + } + + return 0; +} + static int js_host_manifest_error(JSContext *ctx, const char *path, const char *message) { if (path && message) @@ -1512,6 +1693,15 @@ static JSValue js_host_call_wrapper(JSContext *ctx, JSHostResponse resp; JSHostCallResult result = {0}; JSValue ret = JS_EXCEPTION; + JSHostTapeState *tape_state = js_host_get_tape(ctx); + int tape_enabled = tape_state && tape_state->capacity > 0 && tape_state->records; + uint8_t tape_req_hash[32]; + uint8_t tape_resp_hash[32]; + uint32_t tape_req_len = 0; + uint32_t tape_resp_len = 0; + uint64_t tape_gas_pre = 0; + uint64_t tape_gas_post = 0; + int tape_charge_failed = 0; if (!manifest) { JS_ThrowTypeError(ctx, "host manifest is not initialized"); @@ -1592,6 +1782,15 @@ static JSValue js_host_call_wrapper(JSContext *ctx, return JS_EXCEPTION; } + if (tape_enabled) { + memset(tape_req_hash, 0, sizeof(tape_req_hash)); + memset(tape_resp_hash, 0, sizeof(tape_resp_hash)); + tape_req_len = (uint32_t)req_buf.length; + if (js_host_calc_gas_charges(fn, req_buf.length, 0, 0, &tape_gas_pre, NULL)) + tape_gas_pre = 0; + js_sha256(req_buf.data, req_buf.length, tape_req_hash); + } + JS_FreeValue(ctx, args_array); args_array = JS_UNDEFINED; @@ -1618,6 +1817,11 @@ static JSValue js_host_call_wrapper(JSContext *ctx, JS_FreeDVBuffer(ctx, &req_buf); + if (tape_enabled) { + tape_resp_len = (uint32_t)result.length; + js_sha256(result.data, result.length, tape_resp_hash); + } + JSHostResponseValidation validation = { .max_units = fn->max_units, .errors = fn->errors, @@ -1627,7 +1831,29 @@ static JSValue js_host_call_wrapper(JSContext *ctx, if (JS_ParseHostResponse(ctx, result.data, result.length, &validation, &resp)) return JS_EXCEPTION; - if (js_host_charge_post(ctx, fn, result.length, resp.units)) { + if (tape_enabled) { + if (js_host_calc_gas_charges(fn, tape_req_len, tape_resp_len, resp.units, NULL, &tape_gas_post)) + tape_gas_post = 0; + } + + tape_charge_failed = js_host_charge_post(ctx, fn, result.length, resp.units); + + if (tape_enabled) { + JSHostTapeRecord rec = {0}; + rec.fn_id = fn->fn_id; + rec.req_len = tape_req_len; + rec.resp_len = tape_resp_len; + rec.units = resp.units; + rec.gas_pre = tape_gas_pre; + rec.gas_post = tape_gas_post; + rec.is_error = resp.is_error; + rec.charge_failed = tape_charge_failed; + memcpy(rec.req_hash, tape_req_hash, sizeof(rec.req_hash)); + memcpy(rec.resp_hash, tape_resp_hash, sizeof(rec.resp_hash)); + js_host_tape_append(tape_state, &rec); + } + + if (tape_charge_failed) { JS_FreeHostResponse(ctx, &resp); return JS_EXCEPTION; } @@ -1789,12 +2015,13 @@ int JS_InitHostFromManifest(JSContext *ctx, const uint8_t *manifest_bytes, size_ if (js_host_validate_manifest(ctx, manifest_val, &manifest)) goto done; - node = js_malloc(ctx, sizeof(JSHostManifestNode)); + node = js_mallocz(ctx, sizeof(JSHostManifestNode)); if (!node) goto done; node->ctx = ctx; node->manifest = manifest; + node->tape = (JSHostTapeState){0}; node->next = NULL; memset(&manifest, 0, sizeof(manifest)); /* ownership moved */ diff --git a/quickjs-host.h b/quickjs-host.h index 7d1fcf4a0..1b67157a3 100644 --- a/quickjs-host.h +++ b/quickjs-host.h @@ -38,4 +38,25 @@ int JS_InitHostFromManifest(JSContext *ctx, const uint8_t *manifest_bytes, size_ int JS_InitErgonomicGlobals(JSContext *ctx, const uint8_t *context_blob, size_t context_blob_size); void JS_FreeHostManifest(JSContext *ctx); +/* Optional host-call tape (T-043). */ +#define JS_HOST_TAPE_MAX_CAPACITY 1024 + +typedef struct JSHostTapeRecord { + uint32_t fn_id; + uint32_t req_len; + uint32_t resp_len; + uint32_t units; + uint64_t gas_pre; + uint64_t gas_post; + int is_error; + int charge_failed; + uint8_t req_hash[32]; + uint8_t resp_hash[32]; +} JSHostTapeRecord; + +int JS_EnableHostTape(JSContext *ctx, size_t capacity); +int JS_ResetHostTape(JSContext *ctx); +size_t JS_GetHostTapeLength(JSContext *ctx); +int JS_ReadHostTape(JSContext *ctx, JSHostTapeRecord *out_records, size_t max_records, size_t *out_count); + #endif /* QUICKJS_HOST_H */