This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new fdd9a7ba2b29 CAMEL-24152: Fix concurrent marshal/unmarshal data 
corruption in camel-snakeyaml (#24814)
fdd9a7ba2b29 is described below

commit fdd9a7ba2b29fede18929dc427c75486f9d90f93
Author: Federico Mariani <[email protected]>
AuthorDate: Fri Jul 17 14:47:35 2026 +0200

    CAMEL-24152: Fix concurrent marshal/unmarshal data corruption in 
camel-snakeyaml (#24814)
    
    Since CAMEL-22354 (4.15.0), all per-thread Yaml instances shared single
    BaseConstructor and Representer singletons. These SnakeYAML classes are
    heavily stateful and mutated on every load/dump, causing concurrent
    crashes and silent cross-thread data bleed.
    
    Create fresh BaseConstructor, Representer, DumperOptions, and Resolver
    per cached Yaml in getYaml(), restoring per-thread construction.
    
    Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
---
 .../component/snakeyaml/SnakeYAMLDataFormat.java   |  18 +---
 .../snakeyaml/SnakeYAMLConcurrentTest.java         | 104 +++++++++++++++++++++
 2 files changed, 109 insertions(+), 13 deletions(-)

diff --git 
a/components/camel-snakeyaml/src/main/java/org/apache/camel/component/snakeyaml/SnakeYAMLDataFormat.java
 
b/components/camel-snakeyaml/src/main/java/org/apache/camel/component/snakeyaml/SnakeYAMLDataFormat.java
index a8a3937f3e3d..21074bba843d 100644
--- 
a/components/camel-snakeyaml/src/main/java/org/apache/camel/component/snakeyaml/SnakeYAMLDataFormat.java
+++ 
b/components/camel-snakeyaml/src/main/java/org/apache/camel/component/snakeyaml/SnakeYAMLDataFormat.java
@@ -134,18 +134,6 @@ public final class SnakeYAMLDataFormat extends 
ServiceSupport implements DataFor
         if (allowAnyType) {
             typeFilters = "*";
         }
-        if (this.constructor == null) {
-            this.constructor = defaultConstructor(camelContext);
-        }
-        if (this.representer == null) {
-            this.representer = defaultRepresenter();
-        }
-        if (this.dumperOptions == null) {
-            this.dumperOptions = defaultDumperOptions();
-        }
-        if (this.resolver == null) {
-            this.resolver = defaultResolver();
-        }
     }
 
     @Override
@@ -167,7 +155,11 @@ public final class SnakeYAMLDataFormat extends 
ServiceSupport implements DataFor
             options.setTagInspector(new TrustedTagInspector());
             options.setAllowRecursiveKeys(allowRecursiveKeys);
             options.setMaxAliasesForCollections(maxAliasesForCollections);
-            yaml = new Yaml(constructor, representer, dumperOptions, options, 
resolver);
+            BaseConstructor c = constructor != null ? constructor : 
defaultConstructor(camelContext);
+            Representer r = representer != null ? representer : 
defaultRepresenter();
+            DumperOptions d = dumperOptions != null ? dumperOptions : 
defaultDumperOptions();
+            Resolver res = resolver != null ? resolver : defaultResolver();
+            yaml = new Yaml(c, r, d, options, res);
             yamlCache.set(new WeakReference<>(yaml));
         }
 
diff --git 
a/components/camel-snakeyaml/src/test/java/org/apache/camel/component/snakeyaml/SnakeYAMLConcurrentTest.java
 
b/components/camel-snakeyaml/src/test/java/org/apache/camel/component/snakeyaml/SnakeYAMLConcurrentTest.java
new file mode 100644
index 000000000000..f526dba74cca
--- /dev/null
+++ 
b/components/camel-snakeyaml/src/test/java/org/apache/camel/component/snakeyaml/SnakeYAMLConcurrentTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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.camel.component.snakeyaml;
+
+import java.util.ArrayList;
+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 org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.snakeyaml.model.TestPojo;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.fail;
+
+public class SnakeYAMLConcurrentTest extends CamelTestSupport {
+
+    private static final int THREAD_COUNT = 8;
+    private static final int ITERATIONS = 500;
+
+    @Test
+    public void testConcurrentMarshalUnmarshal() throws Exception {
+        ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);
+        CountDownLatch startLatch = new CountDownLatch(1);
+        List<Future<Void>> futures = new ArrayList<>();
+
+        for (int t = 0; t < THREAD_COUNT; t++) {
+            final int threadId = t;
+            futures.add(executor.submit(() -> {
+                startLatch.await();
+                for (int i = 0; i < ITERATIONS; i++) {
+                    String name = "Thread" + threadId + "-Iter" + i;
+                    TestPojo original = new TestPojo(name);
+
+                    String yaml = template.requestBody("direct:marshal", 
original, String.class);
+                    TestPojo result = template.requestBody("direct:unmarshal", 
yaml, TestPojo.class);
+
+                    assertEquals(name, result.getName(),
+                            "Data corruption detected: expected '" + name + "' 
but got '" + result.getName() + "'");
+                }
+                return null;
+            }));
+        }
+
+        startLatch.countDown();
+
+        executor.shutdown();
+        executor.awaitTermination(60, TimeUnit.SECONDS);
+
+        List<Throwable> errors = new ArrayList<>();
+        for (Future<Void> future : futures) {
+            try {
+                future.get();
+            } catch (Exception e) {
+                errors.add(e.getCause() != null ? e.getCause() : e);
+            }
+        }
+
+        if (!errors.isEmpty()) {
+            StringBuilder sb = new StringBuilder();
+            sb.append(errors.size()).append(" thread(s) failed:\n");
+            for (Throwable err : errors) {
+                sb.append("  - ").append(err.getMessage()).append("\n");
+            }
+            fail(sb.toString());
+        }
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        SnakeYAMLDataFormat format = new SnakeYAMLDataFormat(TestPojo.class);
+        format.setAllowAnyType(true);
+
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:marshal")
+                        .marshal(format);
+                from("direct:unmarshal")
+                        .unmarshal(format)
+                        .convertBodyTo(TestPojo.class);
+            }
+        };
+    }
+}

Reply via email to