Skip to content

Conversation

@siganushka
Copy link
Contributor

@siganushka siganushka commented Oct 29, 2025

State Contamination in Marking Stores due to Class-based Getter Cache

Q A
Branch? 7.4
Bug fix? yes
New feature? no
Deprecations? no
License MIT

The issue manifests in Marking Store configurations (e.g., MethodMarkingStore), tracing back to a logic flaw in the core Workflow component's state-accessing mechanism.

The getMarking() method caches the state-accessing Getter Closure based solely on the class name (Subject::class).

In a concurrent, multi-subject environment (e.g., Messenger Workers), this leads to state contamination:

A Worker processes Subject 1, successfully transitioning its state from first_place to second_place.

The cached Getter Closure is created or updated, capturing a reference or memory state related to second_place.

When the same Worker processes Subject 2 (a different instance, whose actual state is first_place in the database), the cached Getter is used.

This Getter incorrectly returns the state related to Subject 1's final marking (second_place), despite Subject 2's data being correct.

Consequently, getMarking() reports a marking of ["second_place":1], causing valid transitions from first_place to be incorrectly blocked.

$subject1 = new Subject('first_place');
$subject2 = new Subject('second_place');

$markingStore = new MethodMarkingStore(true);

$marking1 = $markingStore->getMarking($subject1);
$marking2 = $markingStore->getMarking($subject2);

// Expected: ["first_place" => 1]
// Actual: ["first_place" => 1]
$marking1 ->getPlaces();

// Expected: ["second_place" => 1]
// Actual: ["first_place" => 1]
$marking2 ->getPlaces();

@carsonbot
Copy link

Hey!

Thanks for your PR. You are targeting branch "7.4" but it seems your PR description refers to branch "7.4 bug fixes".
Could you update the PR description or change target branch? This helps core maintainers a lot.

Cheers!

Carsonbot


private static function getSubjectIdentity(object $subject): string
{
return sprintf('%s_%s', $subject::class, spl_object_id($subject));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

even this is not safe, as object ids can be reused after an object is destructed (which is likely to happen in the case of a messenger consumer, otherwise you would be leaking memory)

Copy link
Contributor Author

@siganushka siganushka Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

even this is not safe, as object ids can be reused after an object is destructed (which is likely to happen in the case of a messenger consumer, otherwise you would be leaking memory)

In my use case, when dealing with multiple instances of the same object, the value of getMarking is still influenced by the first object, even if it is the same process.

$foo1 = new Post();
$foo1->setStatus('draft');

$foo2 = new Post();
$foo2->setStatus('published');

$testStateMachine->getMarking($foo1)->getPlaces(); // actual: ['draft' => 1]
$testStateMachine->getMarking($foo2)->getPlaces(); // actual: ['draft' => 1] expected: ['published' => 1]

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you talking about the existing code or your patched code ?

I'm not saying that the existing code is correct. But your patch does not ensure correctness either AFAICT.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you talking about the existing code or your patched code ?

I'm not saying that the existing code is correct. But your patch does not ensure correctness either AFAICT.

Existing code! I fixed this issue in the patch code.

$foo1 = new Post();
$foo1->setStatus('draft');

$foo2 = new Post();
$foo2->setStatus('published');

$testStateMachine->getMarking($foo1)->getPlaces(); // actual: ['draft' => 1]
$testStateMachine->getMarking($foo2)->getPlaces(); // actual: ['draft' => 1] expected: ['published' => 1]

Copy link
Member

@nicolas-grekas nicolas-grekas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed the implementation.

@nicolas-grekas
Copy link
Member

Thank you @siganushka.

@nicolas-grekas nicolas-grekas merged commit 99bbae9 into symfony:7.4 Oct 29, 2025
12 checks passed
@stof
Copy link
Member

stof commented Oct 29, 2025

Caching the getter was implemented in 6.4 in #50974

So this fix needs to be merged in 6.4 rather than in 7.4 only.

@nicolas-grekas
Copy link
Member

Caching the getter was implemented in 6.4 in #50974

IIUC, we only cached the type of getter, not the actual getter closure, so that's fine there (and in 7.3 also)

@stof
Copy link
Member

stof commented Oct 29, 2025

Indeed. #60114 changed the cached information in a broken way, introducing that bug.
However, the fix is incomplete. setters are also affected by the cache contamination.

And btw, the type declaration for properties storing the cache is wrong now as it has not been updated when changing the cached info.

nicolas-grekas added a commit that referenced this pull request Oct 29, 2025
…ache (nicolas-grekas)

This PR was merged into the 7.4 branch.

Discussion
----------

[Workflow] State contamination due to class-based setter cache

| Q             | A
| ------------- | ---
| Branch?       | 7.4
| Bug fix?      | yes
| New feature?  | no
| Deprecations? | no
| Issues        | -
| License       | MIT

Forgotten in #62199

Commits
-------

2bbc09b [Workflow] State contamination due to class-based setter cache
This was referenced Nov 2, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants