[ 
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)

Reply via email to