[
https://issues.apache.org/jira/browse/CAMEL-24152?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Claus Ibsen resolved CAMEL-24152.
---------------------------------
Resolution: Fixed
> 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
> Assignee: Federico Mariani
> Priority: Major
> Labels: ai-assisted-review, dataformat
> Fix For: 4.22.0
>
> 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)