laminas / laminas-servicemanager

Factory-Driven Dependency Injection Container
https://docs.laminas.dev/laminas-servicemanager/
BSD 3-Clause "New" or "Revised" License
152 stars 59 forks source link

Lack of infinite recursion detection for abstract factories #218

Open boesing opened 9 months ago

boesing commented 9 months ago

Bug Report

Q A
Version(s) 3.22.1, 4.0.0*

Summary

There is a lack of infinite recursion detection for both get and has when it comes to abstract factories.

Ref: https://github.com/laminas/laminas-cache/issues/284

Current behavior

When checking a service via has in factories (one abstract factory has to be involved) or receiving a service via get, an infinite recursion along with an application crash is the result.

  1. $container->has('foo');
  2. SM checks all services, aliases and factories for foo
  3. foo not found in services, aliases or factories
  4. SM checks all abstract factories for foo
  5. First abstract factory which checks for $container->has('foo'); will start over No. 1

How to reproduce

use Laminas\ServiceManager\Factory\AbstractFactoryInterface;
use Psr\Container\ContainerInterface;

final class GenericAbstractFactory implements AbstractFactoryInterface
{
    public function canCreate(ContainerInterface $container, $requestedName)
    {
         if (!$container->has('foo')) {
             return false;
         }

          $foo = $container->get('foo');
          // check foo for whatever
          return true; // or false, depending on what $foo represents
    }

    public function __invoke(ContainerInterface $container, $requestedName, ?array $options = null)
    {
         $foo = $container->get('foo'); // we can safely assume that `foo` exists since we checked that in `canCreate` alrady
         // do whatever with foo
         return stdClass(); // return service, maybe with data from foo
    }
}

final class GenericFactory
{
     public function __invoke(ContainerInterface $container): object
     {
          if (!$container->has('foo')) {
              return new stdClass();
          }

          $foo = $container->get('foo');
          // do whatever with foo
          return new stdClass();     
     }
}

$container = new ServiceManager([
     'abstract_factories' => [
         GenericAbstractFactory::class,
      ],
     'factories' => [
         stdClass::class => GenericFactory::class,
     ],
]);

$object = $container->get(stdClass::class); // infinite loop occurs
// unreachable line

Expected behavior

Infinite recursions are prevented from abstract factories.

Ocramius commented 9 months ago

BTW, is this causing an infinite "loop" or infinite "recursion"?

boesing commented 9 months ago

I have to admit that I have no clue if and how there is a difference tbh.

The way I use "recursion" usually is that something is happening within the same class. This issue is actually passing a bunch of classes and methods which in the end leads to the same canCreate method (in the has trace) which then triggers the same process again:

  1. $container->has('foo');
  2. SM checks all services, aliases and factories for foo
  3. foo not found in services, aliases or factories
  4. SM checks all abstract factories for foo
  5. First abstract factory which checks for $container->has('foo'); will start over No. 1

So no clue if this is actually an infinite loop or infinite recursion.

boesing commented 9 months ago

I would implement somthing like this:

https://github.com/laminas/laminas-servicemanager/blob/c97780e74ac5c2fdf2aa4876bccdc718b6fc6ed9/src/ServiceManager.php#L967-L980

       if (isset($this->pendingAbstractFactoryChecks[$name])) { 
           return false; 
       }
       $this->pendingAbstractFactoryChecks[$name] = $name;
       foreach ($this->abstractFactories as $abstractFactory) {
           if ($abstractFactory->canCreate($this->creationContext, $name)) {
              unset($this->pendingAbstractFactoryChecks[$name]);
              return true;
           }
        }

        $resolvedName = $this->aliases[$name] ?? $name;
        if ($resolvedName !== $name) {
            if ($this->abstractFactoryCanCreate($resolvedName)) {
                unset($this->pendingAbstractFactoryChecks[$name]);
                return true;
            }
        }

        unset($this->pendingAbstractFactoryChecks[$name]);
        return false;

But I guess that the problem might be that we have to keep track of which abstract factory is currently being checked and just skip that abstract factory which was called lately so that all but the current one is being checked. So the code above might be too "dumb".

Ocramius commented 9 months ago

I have to admit that I have no clue if and how there is a difference tbh.

infinite loop runs forever. infinite recursion usually leads to a segfault/crash. an infinite loop would be much worse, right now.

boesing commented 9 months ago

I rephrased the issue and replaced loop with recursion. Thx for clarification.