Skip to content

Commit 7c5f4ed

Browse files
authored
Merge pull request #2883 from imhayatunnabi/fix/race-condition-permission-loading
Fix TOCTOU race condition in permission loading for concurrent (Octane etc) environments
2 parents 4ca509e + 3e4474d commit 7c5f4ed

File tree

1 file changed

+36
-8
lines changed

1 file changed

+36
-8
lines changed

src/PermissionRegistrar.php

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ class PermissionRegistrar
4949

5050
private array $wildcardPermissionsIndex = [];
5151

52+
private bool $isLoadingPermissions = false;
53+
5254
/**
5355
* PermissionRegistrar constructor.
5456
*/
@@ -172,6 +174,7 @@ public function clearPermissionsCollection(): void
172174
{
173175
$this->permissions = null;
174176
$this->wildcardPermissionsIndex = [];
177+
$this->isLoadingPermissions = false;
175178
}
176179

177180
/**
@@ -187,24 +190,49 @@ public function clearClassPermissions()
187190
/**
188191
* Load permissions from cache
189192
* And turns permissions array into a \Illuminate\Database\Eloquent\Collection
193+
*
194+
* Thread-safe implementation to prevent race conditions in concurrent environments
195+
* (e.g., Laravel Octane, Swoole, parallel requests)
190196
*/
191-
private function loadPermissions(): void
197+
private function loadPermissions(int $retries = 0): void
192198
{
199+
// First check (without lock) - fast path for already loaded permissions
193200
if ($this->permissions) {
194201
return;
195202
}
196203

197-
$this->permissions = $this->cache->remember(
198-
$this->cacheKey, $this->cacheExpirationTime, fn () => $this->getSerializedPermissionsForCache()
199-
);
204+
// Prevent concurrent loading using a flag-based lock
205+
// This protects against cache stampede and duplicate database queries
206+
if ($this->isLoadingPermissions && $retries < 10) {
207+
// Another thread is loading, wait and retry
208+
usleep(10000); // Wait 10ms
209+
$retries++;
210+
211+
// After wait, recursively check again if permissions were loaded
212+
$this->loadPermissions($retries);
200213

201-
$this->alias = $this->permissions['alias'];
214+
return;
215+
}
202216

203-
$this->hydrateRolesCache();
217+
// Set loading flag to prevent concurrent loads
218+
$this->isLoadingPermissions = true;
204219

205-
$this->permissions = $this->getHydratedPermissionCollection();
220+
try {
221+
$this->permissions = $this->cache->remember(
222+
$this->cacheKey, $this->cacheExpirationTime, fn () => $this->getSerializedPermissionsForCache()
223+
);
206224

207-
$this->cachedRoles = $this->alias = $this->except = [];
225+
$this->alias = $this->permissions['alias'];
226+
227+
$this->hydrateRolesCache();
228+
229+
$this->permissions = $this->getHydratedPermissionCollection();
230+
231+
$this->cachedRoles = $this->alias = $this->except = [];
232+
} finally {
233+
// Always release the loading flag, even if an exception occurs
234+
$this->isLoadingPermissions = false;
235+
}
208236
}
209237

210238
/**

0 commit comments

Comments
 (0)