Memory Safety Issue when using patch or merge on state and assign the result back to state
Description
Tremor is an event processing system for unstructured data. A vulnerability exists between versions 0.7.2 and 0.11.6. This vulnerability is a memory safety Issue when using patch or merge on state and assign the result back to state. In this case, affected versions of Tremor and the tremor-script crate maintains references to memory that might have been freed already. And these memory regions can be accessed by retrieving the state, e.g. send it over TCP or HTTP. This requires the Tremor server (or any other program using tremor-script) to execute a tremor-script script that uses the mentioned language construct. The issue has been patched in version 0.11.6 by removing the optimization and always cloning the target expression of a Merge or Patch. If an upgrade is not possible, a possible workaround is to avoid the optimization by introducing a temporary variable and not immediately reassigning to state.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Memory safety bug in Tremor <0.11.6: state merge/patch leaves dangling references, enabling access to freed memory.
Vulnerability
Tremor versions 0.7.2 through 0.11.5 (and all affected tremor-script crate versions) contain a memory safety vulnerability in the merge and patch language constructs when their result is assigned back to state. The script constructs let state = merge state of end and let state = patch state of end trigger an optimization that manipulates the target value in-place instead of cloning it. Because the internal Value struct borrows string data from the raw bytes of an event, and state persists beyond the lifetime of that event, the optimization leaves references to freed memory [1][2][3].
Exploitation
An attacker must have the ability to provide or influence a tremor-script script that is executed by a Tremor server (or any program using the tremor-script crate) and that contains the vulnerable merge or patch construct with state as the target and a reassignment. No special network position or authentication is required beyond that scripting access; the script runs with the privileges of the Tremor process. The attacker then retrieves the corrupted state (for example, by sending it over TCP or HTTP) to read the freed memory regions [3].
Impact
Successful exploitation allows an attacker to read freed memory regions, which may contain sensitive data from prior events or other memory contents. This constitutes an information disclosure vulnerability. The attacker does not obtain code execution or memory write capabilities from this issue alone [3].
Mitigation
The vulnerability is patched in Tremor version 0.11.6, where the dangerous in-place optimization was removed, and the target expression of a Merge or Patch is always cloned. Users should upgrade to 0.11.6 or later. For those unable to upgrade, a workaround is to avoid the vulnerable pattern by introducing a temporary variable: instead of let state = merge state of ... end, assign the result to a different variable, then manually copy its value to state [2][3].
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
tremor-scriptcrates.io | >= 0.7.3, < 0.11.6 | 0.11.6 |
Affected products
2- tremor-rs/tremor-runtimev5Range: > 0.7.2, < 0.11.6
Patches
11a2efcdbe68eRemove patch and merge in_place optimizations.
19 files changed · +40 −152
tests/script.rs+3 −2 modified@@ -51,6 +51,7 @@ macro_rules! test_cases { out_json.reverse(); let mut results = Vec::new(); + let mut state = Value::null(); for (id, mut json) in in_json.into_iter().enumerate() { let uri = EventOriginUri{ host: "test".into(), @@ -62,7 +63,6 @@ macro_rules! test_cases { }; let context = EventContext::new(id as u64, Some(&uri)); let mut meta = Value::from(Object::default()); - let mut state = Value::null(); match script.run(&context, AggrType::Tick, &mut json, &mut state, &mut meta)? { Return::Drop => (), Return::EmitEvent{..} => results.push(json), @@ -154,6 +154,7 @@ test_cases!( // TODO // const_in_const_lookup, // INSERT + merge_assign_target_state, expr_path, patch_default, patch_default_key, @@ -186,7 +187,7 @@ test_cases!( heredoc_quoted_curly, string_interpolation_import, string_interpolation_prefix, - patch_in_place, + patch_assign_target, tuple_pattern, pattern_cmp, pass_args,
tests/script_runtime_error.rs+3 −3 modified@@ -105,10 +105,10 @@ macro_rules! ignore_cases { file.read_to_string(&mut err)?; let _err = err.trim(); + let mut state = Value::null(); if let Some(mut json) = in_json.pop() { let context = EventContext::new(0, None); let mut meta = Value::object(); - let mut state = Value::null(); let s = script.run(&context, AggrType::Tick, &mut json, &mut state, &mut meta); if let Err(e) = s { let mut h = Dumb::new(); @@ -144,8 +144,8 @@ test_cases!( function_error_n, match_bad_guard_type, match_no_clause_hit, - merge_in_place_new_no_object, - merge_in_place_target_no_object, + merge_assign_target_new_no_object, + merge_assign_target_target_no_object, merge_new_no_object, merge_target_no_object, missing_local,
tests/script_runtime_errors/merge_assign_target_new_no_object/error.txt+0 −0 renamedtests/script_runtime_errors/merge_assign_target_new_no_object/in+0 −0 renamedtests/script_runtime_errors/merge_assign_target_new_no_object/script.tremor+0 −0 renamedtests/script_runtime_errors/merge_assign_target_target_no_object/error.txt+0 −0 renamedtests/script_runtime_errors/merge_assign_target_target_no_object/in+0 −0 renamedtests/script_runtime_errors/merge_assign_target_target_no_object/script.tremor+0 −0 renamedtests/scripts/merge_assign_target_state/in+2 −0 added@@ -0,0 +1,2 @@ +{"foo": "bar"} +{"snot": "badger", "foo": "grmpf"} \ No newline at end of file
tests/scripts/merge_assign_target_state/out+2 −0 added@@ -0,0 +1,2 @@ +{"foo":"bar"} +{"foo":"grmpf","snot":"badger"} \ No newline at end of file
tests/scripts/merge_assign_target_state/script.tremor+8 −0 added@@ -0,0 +1,8 @@ +# initialize state to record, so we can do the merge +let state = match state of + case null => {} + default => state +end; + +let state = merge state of event end; +emit state
tests/scripts/patch_assign_target/in+0 −0 renamedtests/scripts/patch_assign_target/out+0 −0 renamedtests/scripts/patch_assign_target/script.tremor+0 −0 renamedtremor-script/src/ast/base_expr.rs+0 −2 modified@@ -248,8 +248,6 @@ impl<'script> BaseExpr for Expr<'script> { Expr::Emit(e) => e.mid(), Expr::Imut(e) => e.mid(), Expr::Match(e) => e.mid(), - Expr::MergeInPlace(e) => e.mid(), - Expr::PatchInPlace(e) => e.mid(), Expr::IfElse(e) => e.mid(), } }
tremor-script/src/ast/raw.rs+18 −30 modified@@ -18,14 +18,14 @@ use crate::{ ast::{ - base_expr, eq::AstEq, query, upable::Upable, ArrayPattern, ArrayPredicatePattern, - AssignPattern, BinExpr, BinOpKind, Bytes, BytesPart, ClauseGroup, Comprehension, - ComprehensionCase, Costly, DefaultCase, EmitExpr, EventPath, Expr, ExprPath, Expression, - Field, FnDecl, FnDoc, Helper, Ident, IfElse, ImutExpr, ImutExprInt, Invocable, Invoke, - InvokeAggr, InvokeAggrFn, List, Literal, LocalPath, Match, Merge, MetadataPath, ModDoc, - NodeMetas, Patch, PatchOperation, Path, Pattern, PredicateClause, PredicatePattern, Record, - RecordPattern, Recur, ReservedPath, Script, Segment, StatePath, StrLitElement, StringLit, - TestExpr, TuplePattern, UnaryExpr, UnaryOpKind, + base_expr, query, upable::Upable, ArrayPattern, ArrayPredicatePattern, AssignPattern, + BinExpr, BinOpKind, Bytes, BytesPart, ClauseGroup, Comprehension, ComprehensionCase, + Costly, DefaultCase, EmitExpr, EventPath, Expr, ExprPath, Expression, Field, FnDecl, FnDoc, + Helper, Ident, IfElse, ImutExpr, ImutExprInt, Invocable, Invoke, InvokeAggr, InvokeAggrFn, + List, Literal, LocalPath, Match, Merge, MetadataPath, ModDoc, NodeMetas, Patch, + PatchOperation, Path, Pattern, PredicateClause, PredicatePattern, Record, RecordPattern, + Recur, ReservedPath, Script, Segment, StatePath, StrLitElement, StringLit, TestExpr, + TuplePattern, UnaryExpr, UnaryOpKind, }, errors::{ err_generic, error_generic, error_missing_effector, error_oops, Error, ErrorKind, Result, @@ -646,28 +646,16 @@ impl<'script> Upable<'script> for ExprRaw<'script> { let path = a.path.up(helper)?; let mid = helper.add_meta(a.start, a.end); match a.expr.up(helper)? { - Expr::Imut(ImutExprInt::Merge(m)) => { - if path.ast_eq(&m.target) { - Expr::MergeInPlace(Box::new(*m)) - } else { - Expr::Assign { - mid, - path, - expr: Box::new(ImutExprInt::Merge(m).into()), - } - } - } - Expr::Imut(ImutExprInt::Patch(m)) => { - if path.ast_eq(&m.target) { - Expr::PatchInPlace(Box::new(*m)) - } else { - Expr::Assign { - mid, - path, - expr: Box::new(ImutExprInt::Patch(m).into()), - } - } - } + Expr::Imut(ImutExprInt::Merge(m)) => Expr::Assign { + mid, + path, + expr: Box::new(ImutExprInt::Merge(m).into()), + }, + Expr::Imut(ImutExprInt::Patch(m)) => Expr::Assign { + mid, + path, + expr: Box::new(ImutExprInt::Patch(m).into()), + }, expr => Expr::Assign { mid, path,
tremor-script/src/ast.rs+0 −4 modified@@ -961,10 +961,6 @@ pub enum Expr<'script> { Match(Box<Match<'script, Self>>), /// IfElse style match expression IfElse(Box<IfElse<'script, Self>>), - /// In place patch expression - PatchInPlace(Box<Patch<'script>>), - /// In place merge expression - MergeInPlace(Box<Merge<'script>>), /// Assignment expression Assign { /// Id
tremor-script/src/ast/to_static.rs+0 −2 modified@@ -159,8 +159,6 @@ impl<'script> Expr<'script> { match self { Expr::Match(e) => Expr::Match(Box::new(e.into_static())), Expr::IfElse(e) => Expr::IfElse(Box::new(e.into_static())), - Expr::PatchInPlace(e) => Expr::PatchInPlace(Box::new(e.into_static())), - Expr::MergeInPlace(e) => Expr::MergeInPlace(Box::new(e.into_static())), Expr::Assign { mid, path, expr } => Expr::Assign { mid, path: path.into_static(),
tremor-script/src/interpreter/expr.rs+4 −109 modified@@ -13,24 +13,23 @@ // limitations under the License. use super::{ - merge_values, patch_value, resolve, resolve_value, set_local_shadow, test_guard, - test_predicate_expr, Env, ExecOpts, LocalStack, NULL, + resolve, resolve_value, set_local_shadow, test_guard, test_predicate_expr, Env, ExecOpts, + LocalStack, NULL, }; use crate::errors::{ error_assign_array, error_assign_to_const, error_bad_key_err, error_invalid_assign_target, - error_need_obj, error_need_obj_err, error_no_clause_hit, Result, + error_need_obj_err, error_no_clause_hit, Result, }; use crate::prelude::*; use crate::registry::RECUR_PTR; use crate::{ ast::{ BaseExpr, ClauseGroup, ClausePreCondition, Comprehension, DefaultCase, EmitExpr, EventPath, - Expr, IfElse, ImutExprInt, Match, Merge, Patch, Path, Segment, + Expr, IfElse, ImutExprInt, Match, Path, Segment, }, errors::error_oops_err, }; use crate::{stry, Value}; -use matches::matches; use std::mem; use std::{ borrow::{Borrow, Cow}, @@ -219,104 +218,6 @@ impl<'script> Expr<'script> { } } - fn patch_in_place<'run, 'event>( - opts: ExecOpts, - env: &'run Env<'run, 'event>, - event: &'run Value<'event>, - state: &'run Value<'static>, - meta: &'run Value<'event>, - local: &'run LocalStack<'event>, - expr: &'run Patch<'event>, - ) -> Result<Cow<'run, Value<'event>>> { - // This function is called when we encounter code that consumes a value - // to patch it. So the following code: - // ```tremor - // let event = patch event of insert "key" => "value" end - // ``` - // When executed on it's own would clone the event, add a key and - // overwrite original event. - // - // We optimise this as: - // ``` - // patch_in_place event of insert "key" => "value" end - // ``` - // - // This code is generated in impl Upable for ExprRaw where the following - // checks are performed: - // - // 1) the patch is on the RHS of an assignment - // 2) the path of the assigned value and the path of the patched - // expression are identical. - // - // In turn this guarantees (at compile time): - // - // 1) The target (`expr`) is a path lookup - // 2) The target is not a known constant as otherwise the assignment - // will complan - // 3) this leave the `expr` to be either a local, the event, the state, - // metadata or a subkey thereof. - // - // And the following guarantees at run time: - // - // 1) the `expr` is an existing key of the mentioned categories, - // otherwise `expr.target.run` will error. - // 2) `value` will never be owned (however the resolve function is - // generic so it needs to return a Cow) - - let value: Cow<'run, Value<'event>> = - stry!(expr.target.run(opts, env, event, state, meta, local)); - debug_assert!( - !matches!(value, Cow::Owned(_)), - "We should never see a owned value here as patch_in_place is only ever called on existing data in event, state, meta or local" - ); - let v: &Value<'event> = value.borrow(); - // ALLOW: https://github.com/tremor-rs/tremor-runtime/issues/1032 - #[allow(mutable_transmutes, clippy::transmute_ptr_to_ptr)] - // ALLOW: https://github.com/tremor-rs/tremor-runtime/issues/1032 - let v: &mut Value<'event> = unsafe { mem::transmute(v) }; - stry!(patch_value(opts, env, event, state, meta, local, v, expr)); - Ok(value) - } - - fn merge_in_place<'run, 'event>( - &'run self, - opts: ExecOpts, - env: &'run Env<'run, 'event>, - event: &'run mut Value<'event>, - state: &'run mut Value<'static>, - meta: &'run mut Value<'event>, - local: &'run mut LocalStack<'event>, - expr: &'run Merge<'event>, - ) -> Result<Cow<'run, Value<'event>>> { - // Please see the soundness reasoning in `patch_in_place` for details - // those functions perform the same function just with slighty different - // operations. - let value_cow: Cow<'run, Value<'event>> = - stry!(expr.target.run(opts, env, event, state, meta, local)); - debug_assert!( - !matches!(value_cow, Cow::Owned(_)), - "We should never see a owned value here as merge_in_place is only ever called on existing data in event, state, meta or local" - ); - - if value_cow.is_object() { - let value: &Value<'event> = value_cow.borrow(); - // ALLOW: https://github.com/tremor-rs/tremor-runtime/issues/1032 - #[allow(mutable_transmutes, clippy::transmute_ptr_to_ptr)] - // ALLOW: https://github.com/tremor-rs/tremor-runtime/issues/1032 - let value: &mut Value<'event> = unsafe { mem::transmute(value) }; - let replacement = stry!(expr.expr.run(opts, env, event, state, meta, local,)); - - if replacement.is_object() { - stry!(merge_values(self, &expr.expr, value, &replacement)); - Ok(value_cow) - } else { - error_need_obj(self, &expr.expr, replacement.value_type(), env.meta) - } - } else { - error_need_obj(self, &expr.target, value_cow.value_type(), env.meta) - } - } - // TODO: Quite some overlap with `ImutExprInt::comprehension` fn comprehension<'run, 'event>( &'run self, @@ -641,12 +542,6 @@ impl<'script> Expr<'script> { } Expr::Match(ref expr) => self.match_expr(opts, env, event, state, meta, local, expr), Expr::IfElse(ref expr) => self.if_expr(opts, env, event, state, meta, local, expr), - Expr::MergeInPlace(ref expr) => self - .merge_in_place(opts, env, event, state, meta, local, expr) - .map(Cont::Cont), - Expr::PatchInPlace(ref expr) => { - Self::patch_in_place(opts, env, event, state, meta, local, expr).map(Cont::Cont) - } Expr::Comprehension(ref expr) => { self.comprehension(opts, env, event, state, meta, local, expr) }
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-mc22-5q92-8v85ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2021-39228ghsaADVISORY
- github.com/tremor-rs/tremor-runtime/commit/1a2efcdbe68e5e7fd0a05836ac32d2cde78a0b2eghsax_refsource_MISCWEB
- github.com/tremor-rs/tremor-runtime/pull/1217ghsax_refsource_MISCWEB
- github.com/tremor-rs/tremor-runtime/releases/tag/v0.11.6ghsax_refsource_MISCWEB
- github.com/tremor-rs/tremor-runtime/security/advisories/GHSA-mc22-5q92-8v85ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.