From cb6bd8247f5180228a66ca56dabfd030b90ba35c Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Sat, 26 Sep 2026 01:44:50 +0200 Subject: [PATCH 1/2] Forward createValueFromJsonUtf8 in RuntimeDecorator RuntimeDecorator does not forward createValueFromJsonUtf8 to the plain runtime, so decorated runtimes (including WithRuntimeDecorator and the thread-safe Hermes runtime) fall back to the JSON.parse based default in jsi::Runtime instead of the plain runtime's implementation. TracingRuntime relied on that fallback to record JSON parsing, so it now overrides the method and keeps using the default explicitly. Traces are unchanged. --- API/hermes/TracingRuntime.cpp | 8 ++++++ API/hermes/TracingRuntime.h | 7 +++++ API/jsi/jsi/decorator.h | 4 +++ API/jsi/jsi/test/testlib.cpp | 48 ++++++++++++++++++++++++++++++++ unittests/API/APITest.cpp | 10 +++++++ unittests/API/SynthTraceTest.cpp | 20 +++++++++++++ 6 files changed, 97 insertions(+) diff --git a/API/hermes/TracingRuntime.cpp b/API/hermes/TracingRuntime.cpp index 31fac258e98..dbfdce201fa 100644 --- a/API/hermes/TracingRuntime.cpp +++ b/API/hermes/TracingRuntime.cpp @@ -757,6 +757,14 @@ jsi::String TracingRuntime::createStringFromUtf16( return res; } +jsi::Value TracingRuntime::createValueFromJsonUtf8( + const uint8_t *json, + size_t length) { + // Deliberately not RD::createValueFromJsonUtf8, which would parse in the + // plain runtime and return a value that was never recorded. See the header. + return jsi::Runtime::createValueFromJsonUtf8(json, length); +} + jsi::PropNameID TracingRuntime::createPropNameIDFromAscii( const char *str, size_t length) { diff --git a/API/hermes/TracingRuntime.h b/API/hermes/TracingRuntime.h index bc338c9d7c5..59c72b17089 100644 --- a/API/hermes/TracingRuntime.h +++ b/API/hermes/TracingRuntime.h @@ -69,6 +69,13 @@ class TracingRuntime : public jsi::RuntimeDecorator jsi::String createStringFromUtf8(const uint8_t *utf8, size_t length) override; jsi::String createStringFromUtf16(const char16_t *utf16, size_t length) override; + /// Parse JSON with the JS-based default implementation in jsi::Runtime + /// rather than forwarding to the plain runtime. The default calls back into + /// this runtime for global(), getProperty() and call(), so parsing is + /// recorded as ordinary records and the returned value gets a def, without + /// needing a dedicated record type. + jsi::Value createValueFromJsonUtf8(const uint8_t *json, size_t length) + override; std::string utf8(const jsi::PropNameID &) override; jsi::PropNameID createPropNameIDFromAscii(const char *str, size_t length) diff --git a/API/jsi/jsi/decorator.h b/API/jsi/jsi/decorator.h index f101c1832e6..1c42e95f2ec 100644 --- a/API/jsi/jsi/decorator.h +++ b/API/jsi/jsi/decorator.h @@ -236,6 +236,10 @@ class RuntimeDecorator : public Base, private jsi::Instrumentation { return plain_.utf8(s); } + Value createValueFromJsonUtf8(const uint8_t* json, size_t length) override { + return plain_.createValueFromJsonUtf8(json, length); + } + std::u16string utf16(const String& str) override { return plain_.utf16(str); } diff --git a/API/jsi/jsi/test/testlib.cpp b/API/jsi/jsi/test/testlib.cpp index 9c056ef0379..d1b43de0204 100644 --- a/API/jsi/jsi/test/testlib.cpp +++ b/API/jsi/jsi/test/testlib.cpp @@ -1926,6 +1926,54 @@ TEST_P(JSITest, CreateFromUtf16Test) { EXPECT_EQ(cp, 55357); // 0xD83D in decimal } +TEST_P(JSITest, CreateValueFromJsonUtf8Test) { + uint8_t jsonText[] = "{\"a\": 1}"; + + // This Runtime Decorator is used to test the default createValueFromJsonUtf8 + // implementation for VMs that do not provide their own implementation + class DefaultRD : public RuntimeDecorator { + public: + explicit DefaultRD(Runtime& rt) : RuntimeDecorator(rt) {} + + Value createValueFromJsonUtf8(const uint8_t* json, size_t length) override { + return Runtime::createValueFromJsonUtf8(json, length); + } + }; + + DefaultRD defaultRd(rt); + Object obj = + Value::createFromJsonUtf8(defaultRd, jsonText, sizeof(jsonText) - 1) + .getObject(defaultRd); + EXPECT_EQ(obj.getProperty(defaultRd, "a").getNumber(), 1); + + // A RuntimeDecorator must forward createValueFromJsonUtf8 to the runtime it + // decorates, like every other method. + class CountingRD : public RuntimeDecorator { + public: + explicit CountingRD(Runtime& rt) : RuntimeDecorator(rt) {} + + Value createValueFromJsonUtf8(const uint8_t* json, size_t length) override { + ++count; + return plain().createValueFromJsonUtf8(json, length); + } + + int count = 0; + }; + + // Overrides nothing, so it should forward everything to CountingRD. + class OuterRD : public RuntimeDecorator { + public: + explicit OuterRD(Runtime& rt) : RuntimeDecorator(rt) {} + }; + + CountingRD counting(rt); + OuterRD outer(counting); + obj = Value::createFromJsonUtf8(outer, jsonText, sizeof(jsonText) - 1) + .getObject(outer); + EXPECT_EQ(counting.count, 1); + EXPECT_EQ(obj.getProperty(outer, "a").getNumber(), 1); +} + TEST_P(JSITest, GetStringDataTest) { // This Runtime Decorator is used to test the default getStringData // implementation for VMs that do not provide their own implementation diff --git a/unittests/API/APITest.cpp b/unittests/API/APITest.cpp index aae9e34229d..4187e36145e 100644 --- a/unittests/API/APITest.cpp +++ b/unittests/API/APITest.cpp @@ -185,6 +185,16 @@ TEST_P(HermesRuntimeTest, StrictHostFunctionBindTest) { .getBool()); } +TEST_P(HermesRuntimeTest, CreateValueFromJsonUtf8IgnoresGlobalJSON) { + // Hermes parses JSON natively, so replacing the global JSON.parse must not + // change the result, also when the runtime is decorated (e.g. thread-safe). + eval("JSON.parse = function() { return 'hijacked'; }"); + uint8_t json[] = "{\"a\": 1}"; + Value v = Value::createFromJsonUtf8(*rt, json, sizeof(json) - 1); + ASSERT_TRUE(v.isObject()); + EXPECT_EQ(v.getObject(*rt).getProperty(*rt, "a").getNumber(), 1); +} + TEST_P(HermesRuntimeTest, DrainMicrotasksThrowsFinalizationRegistryError) { eval(R"( var target = {}; diff --git a/unittests/API/SynthTraceTest.cpp b/unittests/API/SynthTraceTest.cpp index 6e3b5cf7dbb..736e8899862 100644 --- a/unittests/API/SynthTraceTest.cpp +++ b/unittests/API/SynthTraceTest.cpp @@ -2259,6 +2259,26 @@ TEST_F(SynthTraceReplayTest, CreateJSErrorReplay) { } } +/// JSON parsed from native code must be recorded, so that the tracing runtime +/// knows the resulting object when native code uses it afterwards. +TEST_F(SynthTraceReplayTest, CreateValueFromJsonUtf8Replay) { + { + auto &rt = *traceRt; + uint8_t json[] = "{\"a\": 1, \"b\": \"str\"}"; + jsi::Object obj = jsi::Value::createFromJsonUtf8(rt, json, sizeof(json) - 1) + .getObject(rt); + rt.global().setProperty(rt, "a", obj.getProperty(rt, "a")); + rt.global().setProperty(rt, "parsed", obj); + } + replay(); + { + auto &rt = *replayRt; + EXPECT_EQ(rt.global().getProperty(rt, "a").getNumber(), 1); + jsi::Object parsed = rt.global().getPropertyAsObject(rt, "parsed"); + EXPECT_EQ(parsed.getProperty(rt, "b").getString(rt).utf8(rt), "str"); + } +} + /// This test is here to make sure that the replayed string match is happening /// during replay. TEST_F(SynthTraceReplayTest, PropNameIDUtf8) { From d3ea5bf84d78ad5a0cec975f94761a2dba73e3d9 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Wed, 7 Oct 2026 23:51:09 +0200 Subject: [PATCH 2/2] Keep createValueFromJsonUtf8 public in RuntimeDecorator --- API/jsi/jsi/decorator.h | 7 +++---- API/jsi/jsi/test/testlib.cpp | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/API/jsi/jsi/decorator.h b/API/jsi/jsi/decorator.h index 1c42e95f2ec..7a049ec4c00 100644 --- a/API/jsi/jsi/decorator.h +++ b/API/jsi/jsi/decorator.h @@ -148,6 +148,9 @@ class RuntimeDecorator : public Base, private jsi::Instrumentation { Instrumentation& instrumentation() override { return *this; } + Value createValueFromJsonUtf8(const uint8_t* json, size_t length) override { + return plain_.createValueFromJsonUtf8(json, length); + } protected: // plain is generally going to be a reference to an object managed @@ -236,10 +239,6 @@ class RuntimeDecorator : public Base, private jsi::Instrumentation { return plain_.utf8(s); } - Value createValueFromJsonUtf8(const uint8_t* json, size_t length) override { - return plain_.createValueFromJsonUtf8(json, length); - } - std::u16string utf16(const String& str) override { return plain_.utf16(str); } diff --git a/API/jsi/jsi/test/testlib.cpp b/API/jsi/jsi/test/testlib.cpp index d1b43de0204..52c6e732f00 100644 --- a/API/jsi/jsi/test/testlib.cpp +++ b/API/jsi/jsi/test/testlib.cpp @@ -1968,7 +1968,7 @@ TEST_P(JSITest, CreateValueFromJsonUtf8Test) { CountingRD counting(rt); OuterRD outer(counting); - obj = Value::createFromJsonUtf8(outer, jsonText, sizeof(jsonText) - 1) + obj = outer.createValueFromJsonUtf8(jsonText, sizeof(jsonText) - 1) .getObject(outer); EXPECT_EQ(counting.count, 1); EXPECT_EQ(obj.getProperty(outer, "a").getNumber(), 1);