fix memory leaks (#1350)

* fix a memory leak on thread shutdown

* clean up unused resources at end of request

* try the obvious

* Test

* clang-format

* Also ignores persistent streams.

* Adds stream test.

* Moves clean up function to frankenphp_worker_request_shutdown.

* Fixes test on -nowatcher

* Fixes test on -nowatcher

* Update testdata/file-stream.txt

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>

* Update frankenphp_test.go

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>

---------

Co-authored-by: Alliballibaba <alliballibaba@gmail.com>
Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
This commit is contained in:
Rob Landers
2025-01-25 22:54:04 +01:00
committed by GitHub
parent eee1de147e
commit 05aafc7c44
5 changed files with 68 additions and 20 deletions

View File

@@ -135,6 +135,23 @@ static void frankenphp_worker_request_shutdown() {
zend_end_try();
zend_set_memory_limit(PG(memory_limit));
/*
* free any php_stream resources that are not php source files
* all resources are stored in EG(regular_list), see zend_list.c
*/
zend_resource *val;
ZEND_HASH_FOREACH_PTR(&EG(regular_list), val) {
/* verify the resource is a stream */
if (val->type == php_file_le_stream()) {
php_stream *stream = (php_stream *)val->ptr;
if (stream != NULL && stream->ops != &php_stream_stdio_ops &&
!stream->is_persistent && GC_REFCOUNT(val) == 1) {
zend_list_delete(val);
}
}
}
ZEND_HASH_FOREACH_END();
}
PHPAPI void get_full_env(zval *track_vars_array) {
@@ -746,7 +763,7 @@ void frankenphp_register_variables_from_request_info(
zend_string *path_translated, zend_string *query_string,
zend_string *auth_user, zend_string *request_method) {
frankenphp_register_variable_from_request_info(
content_type, (char *)SG(request_info).content_type, false,
content_type, (char *)SG(request_info).content_type, true,
track_vars_array);
frankenphp_register_variable_from_request_info(
path_translated, (char *)SG(request_info).path_translated, false,
@@ -904,6 +921,9 @@ static void *php_thread(void *arg) {
go_frankenphp_on_thread_shutdown(thread_index);
free(local_ctx);
local_ctx = NULL;
return NULL;
}

View File

@@ -37,14 +37,14 @@ import (
)
type testOptions struct {
workerScript string
watch []string
nbWorkers int
env map[string]string
nbParallelRequests int
realServer bool
logger *zap.Logger
initOpts []frankenphp.Option
workerScript string
watch []string
nbWorkers int
env map[string]string
nbParallelRequests int
realServer bool
logger *zap.Logger
initOpts []frankenphp.Option
}
func runTest(t *testing.T, test func(func(http.ResponseWriter, *http.Request), *httptest.Server, int), opts *testOptions) {
@@ -938,6 +938,21 @@ func testRejectInvalidHeaders(t *testing.T, opts *testOptions) {
}
}
// Worker mode will clean up unreferenced streams between requests
// Make sure referenced streams are not cleaned up
func TestFileStreamInWorkerMode(t *testing.T) {
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, _ int) {
resp1 := fetchBody("GET", "http://example.com/file-stream.php", handler)
assert.Equal(t, resp1, "word1")
resp2 := fetchBody("GET", "http://example.com/file-stream.php", handler)
assert.Equal(t, resp2, "word2")
resp3 := fetchBody("GET", "http://example.com/file-stream.php", handler)
assert.Equal(t, resp3, "word3")
}, &testOptions{workerScript: "file-stream.php", nbParallelRequests: 1, nbWorkers: 1})
}
// To run this fuzzing test use: go test -fuzz FuzzRequest
// TODO: Cover more potential cases
func FuzzRequest(f *testing.F) {
@@ -978,3 +993,13 @@ func FuzzRequest(f *testing.F) {
}, &testOptions{workerScript: "request-headers.php"})
})
}
func fetchBody(method string, url string, handler func(http.ResponseWriter, *http.Request)) string {
req := httptest.NewRequest(method, url, nil)
w := httptest.NewRecorder()
handler(w, req)
resp := w.Result()
body, _ := io.ReadAll(resp.Body)
return string(body)
}

13
testdata/file-stream.php vendored Normal file
View File

@@ -0,0 +1,13 @@
<?php
$fileStream = fopen(__DIR__ . '/file-stream.txt', 'r');
$input = fopen('php://input', 'r');
while (frankenphp_handle_request(function () use ($fileStream, $input) {
echo fread($fileStream, 5);
// this line will lead to a zend_mm_heap corrupted error if the input stream was destroyed
stream_is_local($input);
})) ;
fclose($fileStream);

1
testdata/file-stream.txt vendored Normal file
View File

@@ -0,0 +1 @@
word1word2word3

View File

@@ -3,7 +3,6 @@
package frankenphp_test
import (
"io"
"net/http"
"net/http/httptest"
"os"
@@ -41,16 +40,6 @@ func TestWorkersShouldNotReloadOnExcludingPattern(t *testing.T) {
}, &testOptions{nbParallelRequests: 1, nbWorkers: 1, workerScript: "worker-with-watcher.php", watch: watch})
}
func fetchBody(method string, url string, handler func(http.ResponseWriter, *http.Request)) string {
req := httptest.NewRequest(method, url, nil)
w := httptest.NewRecorder()
handler(w, req)
resp := w.Result()
body, _ := io.ReadAll(resp.Body)
return string(body)
}
func pollForWorkerReset(t *testing.T, handler func(http.ResponseWriter, *http.Request), limit int) bool {
// first we make an initial request to start the request counter
body := fetchBody("GET", "http://example.com/worker-with-watcher.php", handler)