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)