You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@seata.apache.org by ji...@apache.org on 2026/09/28 03:09:16 UTC
(incubator-seata) branch 2.x updated: bugfix: guard concurrent fastjson2 JSONB deserialization (#8278)
This is an automated email from the ASF dual-hosted git repository.
funky-eyes pushed a commit to branch 2.x
in repository https://gitbox.apache.org/repos/asf/incubator-seata.git
The following commit(s) were added to refs/heads/2.x by this push:
new f51f27a978 bugfix: guard concurrent fastjson2 JSONB deserialization (#8278)
f51f27a978 is described below
commit f51f27a9787273a4a525a811ae96e226e2bb99f5
Author: jimin <sl...@163.com>
AuthorDate: Mon Sep 28 11:09:11 2026 +0800
bugfix: guard concurrent fastjson2 JSONB deserialization (#8278)
---
changes/en-us/2.x.md | 2 +
changes/zh-cn/2.x.md | 2 +
.../seata/common/json/Fastjson2JsonbParser.java | 41 ++++++++
.../common/json/Fastjson2JsonbParserTest.java | 115 +++++++++++++++++++++
.../undo/parser/Fastjson2UndoLogParser.java | 3 +-
.../Fastjson2ConcurrentRefDeserializationTest.java | 42 +++++---
.../Fastjson2Serializer.java | 3 +-
.../Fastjson2ConcurrentRefDeserializationTest.java | 41 +++++---
8 files changed, 214 insertions(+), 35 deletions(-)
diff --git a/changes/en-us/2.x.md b/changes/en-us/2.x.md
index 5433973380..692bc536d3 100644
--- a/changes/en-us/2.x.md
+++ b/changes/en-us/2.x.md
@@ -26,6 +26,8 @@ Add changes here for all PR submitted to the 2.x branch.
### bugfix:
+- [[#8278](https://github.com/apache/incubator-seata/pull/8278)] guard concurrent fastjson2 JSONB decoding to preserve RPC and undo-log references
+
- [[#8138](https://github.com/apache/incubator-seata/pull/8138)] fix TCC fence cleanup deleting in-progress/unexpired sibling branch records
- [[#8191](https://github.com/apache/incubator-seata/pull/8191)] fix NetUtil filtered lookups bypassing ignoredInterfaces and preferredNetworks once the default address is cached
- [[#8145](https://github.com/apache/incubator-seata/pull/8145)] fix global lock batch acquire false-failure on Dameng(DM)
diff --git a/changes/zh-cn/2.x.md b/changes/zh-cn/2.x.md
index 8b482bc891..a61b772eae 100644
--- a/changes/zh-cn/2.x.md
+++ b/changes/zh-cn/2.x.md
@@ -26,6 +26,8 @@
### bugfix:
+- [[#8278](https://github.com/apache/incubator-seata/pull/8278)] 保护 Fastjson2 JSONB 并发解码,避免 RPC 与 UndoLog 引用字段丢失
+
- [[#8138](https://github.com/apache/incubator-seata/pull/8138)] 修复 TCC fence 清理时误删同一全局事务中仍在进行(TRIED)或未过期的分支记录的问题
- [[#8191](https://github.com/apache/incubator-seata/pull/8191)] 修复默认本地地址缓存后 NetUtil 参数化查询绕过 ignoredInterfaces 和 preferredNetworks 的问题
- [#8145](https://github.com/apache/incubator-seata/pull/8145) 修复达梦(DM)数据库下全局锁批量获取被误判为失败的问题
diff --git a/json-common/json-common-core/src/main/java/org/apache/seata/common/json/Fastjson2JsonbParser.java b/json-common/json-common-core/src/main/java/org/apache/seata/common/json/Fastjson2JsonbParser.java
new file mode 100644
index 0000000000..3ef60812fc
--- /dev/null
+++ b/json-common/json-common-core/src/main/java/org/apache/seata/common/json/Fastjson2JsonbParser.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.common.json;
+
+import com.alibaba.fastjson2.JSONB;
+import com.alibaba.fastjson2.JSONReader;
+import com.alibaba.fastjson2.filter.Filter;
+
+/**
+ * Serializes Seata JSONB decoding while fastjson2 shares mutable reference-resolution state.
+ * Reader warmup alone does not protect the cached reference paths used during parsing.
+ */
+public final class Fastjson2JsonbParser {
+
+ private Fastjson2JsonbParser() {}
+
+ // Both entry points must use the same monitor: RPC and undo-log parsers share fastjson2 caches.
+ // Keep the entire parse guarded, including the deferred reference-resolution phase.
+ public static synchronized <T> T parseObject(byte[] bytes, Class<T> type, JSONReader.Feature... features) {
+ return JSONB.parseObject(bytes, type, features);
+ }
+
+ public static synchronized <T> T parseObject(
+ byte[] bytes, Class<T> type, Filter filter, JSONReader.Feature... features) {
+ return JSONB.parseObject(bytes, type, filter, features);
+ }
+}
diff --git a/json-common/json-common-core/src/test/java/org/apache/seata/common/json/Fastjson2JsonbParserTest.java b/json-common/json-common-core/src/test/java/org/apache/seata/common/json/Fastjson2JsonbParserTest.java
new file mode 100644
index 0000000000..25f4ddd4fe
--- /dev/null
+++ b/json-common/json-common-core/src/test/java/org/apache/seata/common/json/Fastjson2JsonbParserTest.java
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.common.json;
+
+import com.alibaba.fastjson2.JSONB;
+import com.alibaba.fastjson2.JSONReader;
+import com.alibaba.fastjson2.JSONWriter;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class Fastjson2JsonbParserTest {
+
+ @Test
+ public void concurrentEntryPointsPreserveReferences() throws Exception {
+ byte[] bytes = payload();
+ ExecutorService executor = Executors.newFixedThreadPool(16);
+ CountDownLatch start = new CountDownLatch(1);
+ List<Future<?>> futures = new ArrayList<>();
+ try {
+ for (int i = 0; i < 16; i++) {
+ boolean filtered = i % 2 == 0;
+ futures.add(executor.submit(() -> {
+ assertTrue(start.await(30, TimeUnit.SECONDS));
+ for (int j = 0; j < 25; j++) {
+ SharedReferences value = filtered
+ ? (SharedReferences) Fastjson2JsonbParser.parseObject(
+ bytes,
+ Object.class,
+ JSONReader.autoTypeFilter(SharedReferences.class),
+ JSONReader.Feature.FieldBased)
+ : Fastjson2JsonbParser.parseObject(
+ bytes, SharedReferences.class, JSONReader.Feature.FieldBased);
+ assertReferences(value);
+ }
+ return null;
+ }));
+ }
+ start.countDown();
+ for (Future<?> future : futures) {
+ future.get(30, TimeUnit.SECONDS);
+ }
+ } finally {
+ start.countDown();
+ executor.shutdownNow();
+ assertTrue(executor.awaitTermination(30, TimeUnit.SECONDS));
+ }
+ }
+
+ @Test
+ public void malformedInputDoesNotPreventSubsequentDecoding() throws Exception {
+ assertThrows(
+ RuntimeException.class,
+ () -> Fastjson2JsonbParser.parseObject(
+ new byte[0], SharedReferences.class, JSONReader.Feature.FieldBased));
+ // Use a different thread so a leaked lock cannot be hidden by reentrancy.
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ try {
+ SharedReferences value = executor.submit(() -> Fastjson2JsonbParser.parseObject(
+ payload(), SharedReferences.class, JSONReader.Feature.FieldBased))
+ .get(30, TimeUnit.SECONDS);
+ assertReferences(value);
+ } finally {
+ executor.shutdownNow();
+ assertTrue(executor.awaitTermination(30, TimeUnit.SECONDS));
+ }
+ }
+
+ private static byte[] payload() {
+ SharedReferences value = new SharedReferences();
+ value.first = new ArrayList<>(Arrays.asList("seata"));
+ value.second = value.first;
+ return JSONB.toBytes(
+ value,
+ JSONWriter.Feature.FieldBased,
+ JSONWriter.Feature.WriteClassName,
+ JSONWriter.Feature.ReferenceDetection);
+ }
+
+ private static void assertReferences(SharedReferences value) {
+ assertEquals(Arrays.asList("seata"), value.first);
+ assertSame(value.first, value.second);
+ }
+
+ public static class SharedReferences {
+ private List<String> first;
+ private List<String> second;
+ }
+}
diff --git a/rm-datasource/src/main/java/org/apache/seata/rm/datasource/undo/parser/Fastjson2UndoLogParser.java b/rm-datasource/src/main/java/org/apache/seata/rm/datasource/undo/parser/Fastjson2UndoLogParser.java
index 7849d10ea4..4571edcb72 100644
--- a/rm-datasource/src/main/java/org/apache/seata/rm/datasource/undo/parser/Fastjson2UndoLogParser.java
+++ b/rm-datasource/src/main/java/org/apache/seata/rm/datasource/undo/parser/Fastjson2UndoLogParser.java
@@ -20,6 +20,7 @@ import com.alibaba.fastjson2.JSONB;
import com.alibaba.fastjson2.JSONReader;
import com.alibaba.fastjson2.JSONWriter;
import org.apache.seata.common.executor.Initialize;
+import org.apache.seata.common.json.Fastjson2JsonbParser;
import org.apache.seata.common.json.Fastjson2ObjectReaderWarmup;
import org.apache.seata.common.loader.LoadLevel;
import org.apache.seata.rm.datasource.sql.struct.Field;
@@ -93,6 +94,6 @@ public class Fastjson2UndoLogParser implements UndoLogParser, Initialize {
@Override
public BranchUndoLog decode(byte[] bytes) {
- return JSONB.parseObject(bytes, BranchUndoLog.class, jsonReaderFeature);
+ return Fastjson2JsonbParser.parseObject(bytes, BranchUndoLog.class, jsonReaderFeature);
}
}
diff --git a/rm-datasource/src/test/java/org/apache/seata/rm/datasource/undo/parser/Fastjson2ConcurrentRefDeserializationTest.java b/rm-datasource/src/test/java/org/apache/seata/rm/datasource/undo/parser/Fastjson2ConcurrentRefDeserializationTest.java
index 7bc3768eff..d578156036 100644
--- a/rm-datasource/src/test/java/org/apache/seata/rm/datasource/undo/parser/Fastjson2ConcurrentRefDeserializationTest.java
+++ b/rm-datasource/src/test/java/org/apache/seata/rm/datasource/undo/parser/Fastjson2ConcurrentRefDeserializationTest.java
@@ -52,12 +52,15 @@ public class Fastjson2ConcurrentRefDeserializationTest {
public static void main(String[] args) throws Exception {
Fastjson2UndoLogParser parser =
(Fastjson2UndoLogParser) EnhancedServiceLoader.load(UndoLogParser.class, Fastjson2UndoLogParser.NAME);
+ Fastjson2UndoLogParser otherParser = new Fastjson2UndoLogParser();
+ otherParser.init();
+ Fastjson2UndoLogParser[] parsers = {parser, otherParser};
byte[] bytes = parser.encode(referenceHeavyUndoLog());
- int nullTasks =
- runConcurrentStress(parser, bytes, Integer.getInteger("seata.fastjson2.concurrentRef.threads", 200));
- if (nullTasks > 0) {
- throw new AssertionError("Concurrent deserialization dropped $ref fields: " + nullTasks);
+ int invalidTasks =
+ runConcurrentStress(parsers, bytes, Integer.getInteger("seata.fastjson2.concurrentRef.threads", 200));
+ if (invalidTasks > 0) {
+ throw new AssertionError("Concurrent deserialization lost reference identity: " + invalidTasks);
}
}
@@ -92,41 +95,46 @@ public class Fastjson2ConcurrentRefDeserializationTest {
return tableRecords;
}
- private static int countNullRefFields(BranchUndoLog branchUndoLog) {
+ private static int countInvalidRefFields(BranchUndoLog branchUndoLog) {
if (branchUndoLog == null || branchUndoLog.getSqlUndoLogs() == null) {
return 1;
}
- int nullCount = 0;
+ int invalidCount = 0;
+ TableRecords sharedImage = branchUndoLog.getSqlUndoLogs().get(0).getBeforeImage();
for (SQLUndoLog sqlUndoLog : branchUndoLog.getSqlUndoLogs()) {
if (sqlUndoLog == null) {
- nullCount++;
+ invalidCount++;
continue;
}
if (sqlUndoLog.getBeforeImage() == null
+ || sqlUndoLog.getBeforeImage() != sharedImage
|| sqlUndoLog.getBeforeImage().getRows() == null) {
- nullCount++;
+ invalidCount++;
}
- if (sqlUndoLog.getAfterImage() == null || sqlUndoLog.getAfterImage().getRows() == null) {
- nullCount++;
+ if (sqlUndoLog.getAfterImage() == null
+ || sqlUndoLog.getAfterImage() != sharedImage
+ || sqlUndoLog.getAfterImage().getRows() == null) {
+ invalidCount++;
}
}
- return nullCount;
+ return invalidCount;
}
- private static int runConcurrentStress(Fastjson2UndoLogParser parser, byte[] bytes, int threadCount)
+ private static int runConcurrentStress(Fastjson2UndoLogParser[] parsers, byte[] bytes, int threadCount)
throws Exception {
AtomicReference<Throwable> failure = new AtomicReference<>();
CyclicBarrier barrier = new CyclicBarrier(threadCount);
CountDownLatch endLatch = new CountDownLatch(threadCount);
- AtomicInteger nullTasks = new AtomicInteger();
+ AtomicInteger invalidTasks = new AtomicInteger();
for (int i = 0; i < threadCount; i++) {
+ Fastjson2UndoLogParser parser = parsers[i % parsers.length];
Thread thread = new Thread(
() -> {
try {
barrier.await(CONCURRENT_TEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
- if (countNullRefFields(parser.decode(bytes)) > 0) {
- nullTasks.incrementAndGet();
+ if (countInvalidRefFields(parser.decode(bytes)) > 0) {
+ invalidTasks.incrementAndGet();
}
} catch (Throwable throwable) {
failure.compareAndSet(null, throwable);
@@ -144,12 +152,14 @@ public class Fastjson2ConcurrentRefDeserializationTest {
throw new AssertionError("Concurrent deserialization failed", failure.get());
}
- return nullTasks.get();
+ return invalidTasks.get();
}
private static void assertChildProcessSucceeds() throws Exception {
Process process = new ProcessBuilder(
System.getProperty("java.home") + "/bin/java",
+ "-Dseata.fastjson2.concurrentRef.threads="
+ + Integer.getInteger("seata.fastjson2.concurrentRef.threads", 200),
"-cp",
System.getProperty("java.class.path"),
Fastjson2ConcurrentRefDeserializationTest.class.getName())
diff --git a/serializer/seata-serializer-fastjson2/src/main/java/org.apache.seata.serializer.fastjson2/Fastjson2Serializer.java b/serializer/seata-serializer-fastjson2/src/main/java/org.apache.seata.serializer.fastjson2/Fastjson2Serializer.java
index fcc8cd9c8b..e0179a9ad2 100644
--- a/serializer/seata-serializer-fastjson2/src/main/java/org.apache.seata.serializer.fastjson2/Fastjson2Serializer.java
+++ b/serializer/seata-serializer-fastjson2/src/main/java/org.apache.seata.serializer.fastjson2/Fastjson2Serializer.java
@@ -18,6 +18,7 @@ package org.apache.seata.serializer.fastjson2;
import com.alibaba.fastjson2.JSONB;
import org.apache.seata.common.executor.Initialize;
+import org.apache.seata.common.json.Fastjson2JsonbParser;
import org.apache.seata.common.json.Fastjson2ObjectReaderWarmup;
import org.apache.seata.common.loader.LoadLevel;
import org.apache.seata.core.serializer.Serializer;
@@ -38,7 +39,7 @@ public class Fastjson2Serializer implements Serializer, Initialize {
@Override
public <T> T deserialize(byte[] bytes) {
- return (T) JSONB.parseObject(
+ return (T) Fastjson2JsonbParser.parseObject(
bytes,
Object.class,
Fastjson2SerializerFactory.getInstance().getFilter(),
diff --git a/serializer/seata-serializer-fastjson2/src/test/java/org/apache/seata/serializer/fastjson2/Fastjson2ConcurrentRefDeserializationTest.java b/serializer/seata-serializer-fastjson2/src/test/java/org/apache/seata/serializer/fastjson2/Fastjson2ConcurrentRefDeserializationTest.java
index 2c04ad0896..44f902f3f8 100644
--- a/serializer/seata-serializer-fastjson2/src/test/java/org/apache/seata/serializer/fastjson2/Fastjson2ConcurrentRefDeserializationTest.java
+++ b/serializer/seata-serializer-fastjson2/src/test/java/org/apache/seata/serializer/fastjson2/Fastjson2ConcurrentRefDeserializationTest.java
@@ -47,12 +47,15 @@ public class Fastjson2ConcurrentRefDeserializationTest {
public static void main(String[] args) throws Exception {
Fastjson2Serializer serializer = new Fastjson2Serializer();
((Initialize) serializer).init();
+ Fastjson2Serializer otherSerializer = new Fastjson2Serializer();
+ otherSerializer.init();
+ Fastjson2Serializer[] serializers = {serializer, otherSerializer};
byte[] bytes = serializer.serialize(referenceHeavyMessage());
- int nullTasks = runConcurrentStress(
- serializer, bytes, Integer.getInteger("seata.fastjson2.concurrentRef.threads", 200));
- if (nullTasks > 0) {
- throw new AssertionError("Concurrent deserialization dropped $ref fields: " + nullTasks);
+ int invalidTasks = runConcurrentStress(
+ serializers, bytes, Integer.getInteger("seata.fastjson2.concurrentRef.threads", 200));
+ if (invalidTasks > 0) {
+ throw new AssertionError("Concurrent deserialization lost reference identity: " + invalidTasks);
}
}
@@ -70,41 +73,43 @@ public class Fastjson2ConcurrentRefDeserializationTest {
return message;
}
- private static int countNullRefFields(MergedWarpMessage message) {
+ private static int countInvalidRefFields(MergedWarpMessage message) {
if (message == null || message.msgs == null) {
return 1;
}
- int nullCount = 0;
+ int invalidCount = 0;
+ Object sharedList = ((BatchResultMessage) message.msgs.get(0)).getResultMessages();
for (AbstractMessage child : message.msgs) {
if (!(child instanceof BatchResultMessage)) {
- nullCount++;
+ invalidCount++;
continue;
}
BatchResultMessage batchResult = (BatchResultMessage) child;
- if (batchResult.getResultMessages() == null) {
- nullCount++;
+ if (batchResult.getResultMessages() == null || batchResult.getResultMessages() != sharedList) {
+ invalidCount++;
}
- if (batchResult.getMsgIds() == null) {
- nullCount++;
+ if (batchResult.getMsgIds() == null || batchResult.getMsgIds() != sharedList) {
+ invalidCount++;
}
}
- return nullCount;
+ return invalidCount;
}
- private static int runConcurrentStress(Fastjson2Serializer serializer, byte[] bytes, int threadCount)
+ private static int runConcurrentStress(Fastjson2Serializer[] serializers, byte[] bytes, int threadCount)
throws Exception {
AtomicReference<Throwable> failure = new AtomicReference<>();
CyclicBarrier barrier = new CyclicBarrier(threadCount);
CountDownLatch endLatch = new CountDownLatch(threadCount);
- AtomicInteger nullTasks = new AtomicInteger();
+ AtomicInteger invalidTasks = new AtomicInteger();
for (int i = 0; i < threadCount; i++) {
+ Fastjson2Serializer serializer = serializers[i % serializers.length];
Thread thread = new Thread(
() -> {
try {
barrier.await(CONCURRENT_TEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
- if (countNullRefFields((MergedWarpMessage) serializer.deserialize(bytes)) > 0) {
- nullTasks.incrementAndGet();
+ if (countInvalidRefFields((MergedWarpMessage) serializer.deserialize(bytes)) > 0) {
+ invalidTasks.incrementAndGet();
}
} catch (Throwable throwable) {
failure.compareAndSet(null, throwable);
@@ -122,12 +127,14 @@ public class Fastjson2ConcurrentRefDeserializationTest {
throw new AssertionError("Concurrent deserialization failed", failure.get());
}
- return nullTasks.get();
+ return invalidTasks.get();
}
private static void assertChildProcessSucceeds() throws Exception {
Process process = new ProcessBuilder(
System.getProperty("java.home") + "/bin/java",
+ "-Dseata.fastjson2.concurrentRef.threads="
+ + Integer.getInteger("seata.fastjson2.concurrentRef.threads", 200),
"-cp",
System.getProperty("java.class.path"),
Fastjson2ConcurrentRefDeserializationTest.class.getName())
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@seata.apache.org
For additional commands, e-mail: notifications-help@seata.apache.org