Federico Mariani created CAMEL-24152:
----------------------------------------

             Summary: camel-snakeyaml - Concurrent marshal/unmarshal corrupts 
data: per-thread Yaml instances share one stateful constructor/representer
                 Key: CAMEL-24152
                 URL: https://issues.apache.org/jira/browse/CAMEL-24152
             Project: Camel
          Issue Type: Bug
          Components: camel-snakeyaml
            Reporter: Federico Mariani
         Attachments: SnakeYAMLConcurrentSharedConstructorIssueTest.java

*Severity:* Critical (concurrent data corruption / cross-message data bleed)
*Affects:* 4.15.0 through 4.22.0-SNAPSHOT (regression)

h3. Problem
{{SnakeYAMLDataFormat.getYaml()}} caches one {{Yaml}} per thread via 
{{ThreadLocal<WeakReference<Yaml>>}} precisely because {{Yaml}} is not 
thread-safe. However, every per-thread {{Yaml}} is constructed from the *same 
shared* {{constructor}} ({{BaseConstructor}}) and {{representer}} 
({{Representer}}) instance fields, created once in {{doStart()}}:

{code:java}
yaml = new Yaml(constructor, representer, dumperOptions, options, resolver); // 
shared constructor/representer
{code}

SnakeYAML's {{BaseConstructor}} and {{BaseRepresenter}} are heavily stateful 
(composer, constructedObjects, recursiveObjects, maps2fill, rootTag / 
representedObjects, objectToRepresent) and are mutated on every 
{{load}}/{{dump}}. Sharing them across threads defeats the per-thread {{Yaml}} 
cache entirely.

h3. Regression
Introduced by CAMEL-22354 (commit {{5dfc5c2857e9}}, first released in 4.15.0). 
Before that commit the fields were {{Function<CamelContext, BaseConstructor>}} 
etc. and {{getYaml()}} called {{constructor.apply(context)}}, producing a 
*fresh* constructor per thread. The refactor flattened them to shared 
singletons while keeping the ThreadLocal cache, silently removing the invariant 
the cache exists to protect.

h3. Failure scenario
Any concurrent YAML route (platform-http/jetty consumer, {{seda}} with 
{{concurrentConsumers}}, parallel splitter). Under concurrency the shared state 
corrupts in three ways, all reproduced by the attached test (8 threads x 2000 
iterations):
* internal SnakeYAML crashes on unmarshal ({{IndexOutOfBoundsException}}, 
{{ArrayIndexOutOfBoundsException}}, {{ParserException}}, NPE on null token),
* crashes on the marshal path ({{NullPointerException: Nodes must be 
provided}}),
* *silent cross-thread bleed*: an unmarshal returns a document parsed from 
another thread's input (e.g. {{expected key=t6i672 but got key=t4i708}}) — one 
exchange receiving another exchange's payload.

h3. Suggested fix
Build a fresh default constructor/representer per cached {{Yaml}} inside 
{{getYaml()}} (restore per-thread construction). For a user-supplied 
{{constructor}}/{{representer}}, either document that it must be thread-safe or 
also construct per-thread.

h3. Reproducer
Attached {{SnakeYAMLConcurrentSharedConstructorIssueTest.java}} asserts the 
correct behavior (no thread throws; each unmarshal returns exactly the value 
that thread submitted) and is RED on current code (569/58/50 distinct failures 
across three runs).

----
_Reported by Claude Code on behalf of Federico Mariani (Croway). A failing 
JUnit reproducer (confirmed RED on 4.22.0-SNAPSHOT) is attached._



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to