From 76a6dd258733f19a6f1c6355252f3c0abba003d1 Mon Sep 17 00:00:00 2001 From: Hayatunnabi Nabil Date: Mon, 13 Oct 2025 17:14:31 +0600 Subject: [PATCH 1/3] fix:Critical race condition in permission loading for concurrent environments --- src/PermissionRegistrar.php | 49 +++++++++++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index 92e6edc4e..eaf9733e8 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -49,6 +49,8 @@ class PermissionRegistrar private array $wildcardPermissionsIndex = []; + private bool $isLoadingPermissions = false; + /** * PermissionRegistrar constructor. */ @@ -172,6 +174,7 @@ public function clearPermissionsCollection(): void { $this->permissions = null; $this->wildcardPermissionsIndex = []; + $this->isLoadingPermissions = false; } /** @@ -187,24 +190,56 @@ public function clearClassPermissions() /** * Load permissions from cache * And turns permissions array into a \Illuminate\Database\Eloquent\Collection + * + * Thread-safe implementation to prevent race conditions in concurrent environments + * (e.g., Laravel Octane, Swoole, parallel requests) */ private function loadPermissions(): void { + // First check (without lock) - fast path for already loaded permissions if ($this->permissions) { return; } - $this->permissions = $this->cache->remember( - $this->cacheKey, $this->cacheExpirationTime, fn () => $this->getSerializedPermissionsForCache() - ); + // Prevent concurrent loading using a flag-based lock + // This protects against cache stampede and duplicate database queries + if ($this->isLoadingPermissions) { + // Another thread is loading, wait and retry + usleep(10000); // Wait 10ms + + // Recursive retry with loaded permissions check + if ($this->permissions) { + return; + } - $this->alias = $this->permissions['alias']; + // If still not loaded after wait, proceed normally + // The cache->remember() will handle cache-level locking + } - $this->hydrateRolesCache(); + // Set loading flag to prevent concurrent loads + $this->isLoadingPermissions = true; - $this->permissions = $this->getHydratedPermissionCollection(); + try { + // Double-check after acquiring lock + if ($this->permissions) { + return; + } - $this->cachedRoles = $this->alias = $this->except = []; + $this->permissions = $this->cache->remember( + $this->cacheKey, $this->cacheExpirationTime, fn () => $this->getSerializedPermissionsForCache() + ); + + $this->alias = $this->permissions['alias']; + + $this->hydrateRolesCache(); + + $this->permissions = $this->getHydratedPermissionCollection(); + + $this->cachedRoles = $this->alias = $this->except = []; + } finally { + // Always release the loading flag, even if an exception occurs + $this->isLoadingPermissions = false; + } } /** From a1a2d745e91dd5674dfa5e00b4eca9530785c763 Mon Sep 17 00:00:00 2001 From: Hayatunnabi Nabil Date: Tue, 14 Oct 2025 10:34:42 +0600 Subject: [PATCH 2/3] refactor: Simplify permission loading logic to enhance concurrency handling --- src/PermissionRegistrar.php | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index eaf9733e8..a703e1c04 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -207,24 +207,16 @@ private function loadPermissions(): void // Another thread is loading, wait and retry usleep(10000); // Wait 10ms - // Recursive retry with loaded permissions check - if ($this->permissions) { - return; - } + // After wait, recursively check again if permissions were loaded + $this->loadPermissions(); - // If still not loaded after wait, proceed normally - // The cache->remember() will handle cache-level locking + return; } // Set loading flag to prevent concurrent loads $this->isLoadingPermissions = true; try { - // Double-check after acquiring lock - if ($this->permissions) { - return; - } - $this->permissions = $this->cache->remember( $this->cacheKey, $this->cacheExpirationTime, fn () => $this->getSerializedPermissionsForCache() ); From 3e4474d50f500fd79ce47c88e1bb5cee0c0d5577 Mon Sep 17 00:00:00 2001 From: Hayatunnabi Nabil Date: Thu, 23 Oct 2025 18:24:51 +0600 Subject: [PATCH 3/3] ref: enhance permission loading with retry mechanism for improved concurrency --- src/PermissionRegistrar.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php index a703e1c04..2f9479eb8 100644 --- a/src/PermissionRegistrar.php +++ b/src/PermissionRegistrar.php @@ -194,7 +194,7 @@ public function clearClassPermissions() * Thread-safe implementation to prevent race conditions in concurrent environments * (e.g., Laravel Octane, Swoole, parallel requests) */ - private function loadPermissions(): void + private function loadPermissions(int $retries = 0): void { // First check (without lock) - fast path for already loaded permissions if ($this->permissions) { @@ -203,12 +203,13 @@ private function loadPermissions(): void // Prevent concurrent loading using a flag-based lock // This protects against cache stampede and duplicate database queries - if ($this->isLoadingPermissions) { + if ($this->isLoadingPermissions && $retries < 10) { // Another thread is loading, wait and retry usleep(10000); // Wait 10ms + $retries++; // After wait, recursively check again if permissions were loaded - $this->loadPermissions(); + $this->loadPermissions($retries); return; }