Skip to content

Commit 99bbae9

Browse files
bug #62199 [Workflow] State Contamination in Marking Stores due to Class-based Getter Cache (siganushka)
This PR was merged into the 7.4 branch. Discussion ---------- [Workflow] State Contamination in Marking Stores due to Class-based Getter Cache 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. ```php $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(); ``` Commits ------- 160608d fix: fixed State contamination in marking stores due to class-based getter cache
2 parents e71f7d3 + 160608d commit 99bbae9

File tree

2 files changed

+20
-3
lines changed

2 files changed

+20
-3
lines changed

src/Symfony/Component/Workflow/MarkingStore/MethodMarkingStore.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public function getMarking(object $subject): Marking
5151
{
5252
$marking = null;
5353
try {
54-
$marking = ($this->getters[$subject::class] ??= $this->getGetter($subject))();
54+
$marking = ($this->getters[$subject::class] ??= $this->getGetter($subject))($subject);
5555
} catch (\Error $e) {
5656
$unInitializedPropertyMessage = \sprintf('Typed property %s::$%s must not be accessed before initialization', get_debug_type($subject), $this->property);
5757
if ($e->getMessage() !== $unInitializedPropertyMessage) {
@@ -93,8 +93,8 @@ private function getGetter(object $subject): callable
9393
$method = 'get'.ucfirst($property);
9494

9595
return match (self::getType($subject, $property, $method)) {
96-
MarkingStoreMethod::METHOD => $subject->{$method}(...),
97-
MarkingStoreMethod::PROPERTY => static fn () => $subject->{$property},
96+
MarkingStoreMethod::METHOD => static fn ($subject) => $subject->{$method}(),
97+
MarkingStoreMethod::PROPERTY => static fn ($subject) => $subject->{$property},
9898
};
9999
}
100100

src/Symfony/Component/Workflow/Tests/MarkingStore/MethodMarkingStoreTest.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,23 @@ public function testGetMarkingWithUninitializedProperty2()
125125
$markingStore->getMarking($subject);
126126
}
127127

128+
public function testGetMarkingWithSameSubjectMultipleTimes()
129+
{
130+
$subject1 = new Subject('first_place');
131+
$subject2 = new Subject('second_place');
132+
$subject3 = new Subject('third_place');
133+
134+
$markingStore = new MethodMarkingStore(true);
135+
136+
$marking1 = $markingStore->getMarking($subject1);
137+
$marking2 = $markingStore->getMarking($subject2);
138+
$marking3 = $markingStore->getMarking($subject3);
139+
140+
$this->assertSame(['first_place' => 1], $marking1->getPlaces());
141+
$this->assertSame(['second_place' => 1], $marking2->getPlaces());
142+
$this->assertSame(['third_place' => 1], $marking3->getPlaces());
143+
}
144+
128145
private function createValueObject(string $markingValue): object
129146
{
130147
return new class($markingValue) {

0 commit comments

Comments
 (0)