Skip to content

memory leak when using fopen with a stream wrapper #8548

Closed

Description

@manueldimmler

Description

The following code takes up a lot of memory and keeps increasing in a endless loop:

#!/usr/bin/php
<?php

class Wrapper
{

    public function stream_open(string $path, string $mode, int $options): bool
    {
        return true;
    }

}

$filename = tempnam(__DIR__, '');
touch($filename);

$wrapper = new Wrapper();
$counter = 0;
while ($counter < 1_000_000) {
    $counter++;
    stream_wrapper_register('symfony', Wrapper::class);
    fopen('symfony://' . $filename, 'r');
    stream_wrapper_unregister('symfony');
}

memory_leak_01

With a small change, the problem disappears:

#!/usr/bin/php
<?php

class Wrapper
{

    public function stream_open(string $path, string $mode, int $options): bool
    {
        return true;
    }

}

$filename = tempnam(__DIR__, '');
touch($filename);

$wrapper = new Wrapper();
$counter = 0;
stream_wrapper_register('symfony', Wrapper::class);
while ($counter < 1_000_000) {
    $counter++;
    fopen('symfony://' . $filename, 'r');
}
stream_wrapper_unregister('symfony');

memory_leak_02

The memory leak was described in the following ticket: symfony/symfony#46256

PHP Version

PHP 8.1.5 (cli)

Operating System

Ubuntu 20.04

Activity

  1. cmb69 commented on May 16, 2022

    @cmb69
    Member

    The problem is that stream_wrapper_register() allocates a resouce, but that is not deleted in stream_wrapper_unregister() (but only during shutdown).

    Note that even when this will be fixed, it's still not a good idea to repeatly register and unregister stream wrappers, because of potential resource ID depletion.

  2. self-assigned this
    on May 19, 2022
  3. olsavmic commented on Aug 1, 2022

    @olsavmic
    Contributor

    @iluuu1994 Is there any possibility of backporting/releasing this in 8.0.*/8.1.*?

    Thanks!

  4. iluuu1994 commented on Aug 1, 2022

    @iluuu1994
    Member

    @olsavmic Since this is a rather theoretical and old leak I decided not to. Are you hitting this issue?

  5. olsavmic commented on Aug 1, 2022

    @olsavmic
    Contributor

    Yes, pretty heavily due to symfony/http-client (symfony/symfony#46256). We have switched to guzzle client implementation which does not suffer from this issue so it's no longer critical for us but it's still an issue.

    I will suggest a singleton-like approach for the stream_wrapper_registrater call in the Symfony repository but I don't see into the internals of streams and linux resource ids. I'd assume that resource id is freed after unregistration but perhaps that's not the case?

  6. iluuu1994 commented on Aug 1, 2022

    @iluuu1994
    Member

    Oh, I wasn't aware Symfony actually unregisters the stream wrapper on every call.

    @nicolas-grekas Is unregistering really necessary or could Symfony check if the stream wrapper has already been registered?

    If not we should probably be able to back-port a5a89cc and c019421 since there aren't any public ABI breaks. @cmb69 Would that be ok for you?

  7. cmb69 commented on Aug 1, 2022

    @cmb69
    Member

    I'm not hard against back-porting, but I really think this should be fixed in userland; repeatedly registering and unregistering stream wrappers is not a good idea anyway, because of potential resource ID depletion.

  8. nicolas-grekas commented on Aug 1, 2022

    @nicolas-grekas
    Contributor

    The goal of this code is to not leak anything in the global scope (aka not allow anyone but internal code to fopen() that kind of streams.) I didn't know this had any effect on resource ids. I'll look for changing this.

  9. cmb69 commented on Aug 1, 2022

    @cmb69
    Member

    I didn't know this had any effect on resource ids.

    All resources (regardless of kind) are internally stored in a hash table (see get_resources()), and are basically managed with append ($a[] = …) and unset operations. As of PHP 8.1.0, resource ID depletion is actually no longer a concern, but prior to that version, there couldn't be more than INT_MAX resources, what could cause real issues for long running processes.

  10. nicolas-grekas commented on Aug 1, 2022

    @nicolas-grekas
    Contributor

    Sure, but what's the relationship with repeatedly registering and unregistering stream wrappers?

  11. cmb69 commented on Aug 1, 2022

    @cmb69
    Member

    stream_wrapper_register() registers a new resource internally:

    rsrc = zend_register_resource(uwrap, le_protocols);

  12. cmb69 commented on Aug 1, 2022

    @cmb69
    Member

    stream_wrapper_register() registers a new resource internally:

    rsrc = zend_register_resource(uwrap, le_protocols);

    PS: might be worth documenting this.

  13. nicolas-grekas commented on Aug 1, 2022

    @nicolas-grekas
    Contributor

    Thanks for the hints. This should be fixed for Symfony in symfony/symfony#47143

  14. added a commit that references this issue on Aug 1, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions