Merge ~athos-ribeiro/ubuntu/+source/php8.1:lp2017207-leak-jammy into ubuntu/+source/php8.1:ubuntu/jammy-devel

Proposed by Athos Ribeiro
Status: Merged
Approved by: git-ubuntu bot
Approved revision: not available
Merged at revision: 28b3da5b035b0b40fdf3f87542b633c721e158ee
Proposed branch: ~athos-ribeiro/ubuntu/+source/php8.1:lp2017207-leak-jammy
Merge into: ubuntu/+source/php8.1:ubuntu/jammy-devel
Diff against target: 229 lines (+207/-0)
3 files modified
debian/changelog (+7/-0)
debian/patches/fix-map-ptr-mem-leak.patch (+199/-0)
debian/patches/series (+1/-0)
Reviewer Review Type Date Requested Status
git-ubuntu bot Approve
Sergio Durigan Junior (community) Approve
Canonical Server Reporter Pending
Review via email: mp+444861@code.launchpad.net

Description of the change

Fix for LP: #2017207.

PPA: https://launchpad.net/~athos-ribeiro/+archive/ubuntu/php-fpm-leak/+packages

DEP8 test suite run result:

  - php8.1/8.1.2-1ubuntu2.12~ppa1
    + ✅ php8.1 on jammy for amd64 @ 15.06.23 13:09:26 Log️ 🗒️
    + ✅ php8.1 on jammy for arm64 @ 15.06.23 13:14:31 Log️ 🗒️
    + ✅ php8.1 on jammy for i386 @ 15.06.23 13:08:49 Log️ 🗒️
    + ✅ php8.1 on jammy for ppc64el @ 15.06.23 13:08:30 Log️ 🗒️
    + ✅ php8.1 on jammy for s390x @ 15.06.23 13:22:41 Log️ 🗒️
    + ✅ php8.1 on jammy for armhf @ 15.06.23 16:47:53 Log️ 🗒️

To post a comment you must log in.
Revision history for this message
Sergio Durigan Junior (sergiodj) wrote :

Thanks, Athos.

I was able to reproduce the problem following your Test Plan, and then verify that the patch indeed fixes the issue. I'm wondering if it'd be possible to improve the Test Plan so as to make use of valgrind or some other tool that could more explicitly point to memory leaks, but I came to the conclusion that it's just not worth the hassle.

I think it'd be a good idea to mention in the Test Plan that "htop -p PID" can be used to monitor a specific php-fm process. Otherwise, LGTM. +1

review: Approve
Revision history for this message
git-ubuntu bot (git-ubuntu-bot) wrote :

Approvers: athos-ribeiro, sergiodj
Uploaders: athos-ribeiro, sergiodj
MP auto-approved

review: Approve
Revision history for this message
Athos Ribeiro (athos-ribeiro) wrote :

Thanks, Sergio. Uploaded and am changing the test plan to include your htop hint.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/debian/changelog b/debian/changelog
2index d63e452..3e57dd7 100644
3--- a/debian/changelog
4+++ b/debian/changelog
5@@ -1,3 +1,10 @@
6+php8.1 (8.1.2-1ubuntu2.12) jammy; urgency=medium
7+
8+ * d/p/fix-map-ptr-mem-leak.patch: Fix map_ptr opcache-less fpm memory leak.
9+ (LP: #2017207)
10+
11+ -- Athos Ribeiro <athos.ribeiro@canonical.com> Wed, 14 Jun 2023 19:57:19 -0300
12+
13 php8.1 (8.1.2-1ubuntu2.11) jammy-security; urgency=medium
14
15 * SECURITY UPDATE: password_verify() accepts invalid Blowfish hashes
16diff --git a/debian/patches/fix-map-ptr-mem-leak.patch b/debian/patches/fix-map-ptr-mem-leak.patch
17new file mode 100644
18index 0000000..c9e26af
19--- /dev/null
20+++ b/debian/patches/fix-map-ptr-mem-leak.patch
21@@ -0,0 +1,199 @@
22+From ff62d117a35509699f8bac8b0750a956914da1b7 Mon Sep 17 00:00:00 2001
23+From: Niels Dossche <7771979+nielsdos@users.noreply.github.com>
24+Date: Sat, 4 Mar 2023 23:22:05 +0100
25+Subject: [PATCH] Fix GH-8646: Memory leak PHP FPM 8.1
26+
27+Fixes GH-8646
28+See https://github.com/php/php-src/issues/8646 for thorough discussion.
29+
30+Interned strings that hold class entries can get a corresponding slot in map_ptr for the CE cache.
31+map_ptr works like a bump allocator: there is a counter which increases to allocate the next slot in the map.
32+
33+For class name strings in non-opcache we have:
34+ - on startup: permanent + interned
35+ - on request: interned
36+For class name strings in opcache we have:
37+ - on startup: permanent + interned
38+ - on request: either not interned at all, which we can ignore because they won't get a CE cache entry
39+ or they were already permanent + interned
40+ or we get a new permanent + interned string in the opcache persistence code
41+
42+Notice that the map_ptr layout always has the permanent strings first, and the request strings after.
43+In non-opcache, a request string may get a slot in map_ptr, and that interned request string
44+gets destroyed at the end of the request. The corresponding map_ptr slot can thereafter never be used again.
45+This causes map_ptr to keep reallocating to larger and larger sizes.
46+
47+We solve it as follows:
48+We can check whether we had any interned request strings, which only happens in non-opcache.
49+If we have any, we reset map_ptr to the last permanent string.
50+We can't lose any permanent strings because of map_ptr's layout.
51+
52+Closes GH-10783.
53+
54+Bug: https://github.com/php/php-src/issues/8646
55+Bug-Ubuntu: https://bugs.launchpad.net/ubuntu/+source/php8.1/+bug/2017207
56+Origin: backport, https://github.com/php/php-src/commit/ff62d117a35509699f8bac8b0750a956914da1b7
57+Last-Update: 2023-06-14
58+---
59+ NEWS | 1 +
60+ Zend/zend.c | 28 +++++++++++++++++++
61+ ext/zend_test/test.c | 6 +++++
62+ ext/zend_test/test.stub.php | 2 ++
63+ ext/zend_test/test_arginfo.h | 10 ++++---
64+ sapi/fpm/tests/gh8646.phpt | 52 ++++++++++++++++++++++++++++++++++++
65+ 6 files changed, 96 insertions(+), 3 deletions(-)
66+ create mode 100644 sapi/fpm/tests/gh8646.phpt
67+
68+--- a/Zend/zend.c
69++++ b/Zend/zend.c
70+@@ -1268,6 +1268,34 @@
71+
72+ zend_destroy_rsrc_list(&EG(regular_list));
73+
74++ /* See GH-8646: https://github.com/php/php-src/issues/8646
75++ *
76++ * Interned strings that hold class entries can get a corresponding slot in map_ptr for the CE cache.
77++ * map_ptr works like a bump allocator: there is a counter which increases to allocate the next slot in the map.
78++ *
79++ * For class name strings in non-opcache we have:
80++ * - on startup: permanent + interned
81++ * - on request: interned
82++ * For class name strings in opcache we have:
83++ * - on startup: permanent + interned
84++ * - on request: either not interned at all, which we can ignore because they won't get a CE cache entry
85++ * or they were already permanent + interned
86++ * or we get a new permanent + interned string in the opcache persistence code
87++ *
88++ * Notice that the map_ptr layout always has the permanent strings first, and the request strings after.
89++ * In non-opcache, a request string may get a slot in map_ptr, and that interned request string
90++ * gets destroyed at the end of the request. The corresponding map_ptr slot can thereafter never be used again.
91++ * This causes map_ptr to keep reallocating to larger and larger sizes.
92++ *
93++ * We solve it as follows:
94++ * We can check whether we had any interned request strings, which only happens in non-opcache.
95++ * If we have any, we reset map_ptr to the last permanent string.
96++ * We can't lose any permanent strings because of map_ptr's layout.
97++ */
98++ if (zend_hash_num_elements(&CG(interned_strings)) > 0) {
99++ zend_map_ptr_reset();
100++ }
101++
102+ #if GC_BENCH
103+ fprintf(stderr, "GC Statistics\n");
104+ fprintf(stderr, "-------------\n");
105+--- a/ext/zend_test/test.c
106++++ b/ext/zend_test/test.c
107+@@ -278,6 +278,12 @@
108+ RETURN_TRUE;
109+ }
110+
111++static ZEND_FUNCTION(zend_get_map_ptr_last)
112++{
113++ ZEND_PARSE_PARAMETERS_NONE();
114++ RETURN_LONG(CG(map_ptr_last));
115++}
116++
117+ static zend_object *zend_test_class_new(zend_class_entry *class_type)
118+ {
119+ zend_object *obj = zend_objects_new(class_type);
120+--- a/ext/zend_test/test.stub.php
121++++ b/ext/zend_test/test.stub.php
122+@@ -93,6 +93,8 @@
123+ function zend_weakmap_dump(): array {}
124+
125+ function zend_get_unit_enum(): ZendTestUnitEnum {}
126++
127++ function zend_get_map_ptr_last(): int {}
128+ }
129+
130+ namespace ZendTestNS {
131+--- a/ext/zend_test/test_arginfo.h
132++++ b/ext/zend_test/test_arginfo.h
133+@@ -65,12 +65,14 @@
134+ ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_zend_get_unit_enum, 0, 0, ZendTestUnitEnum, 0)
135+ ZEND_END_ARG_INFO()
136+
137+-ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_ZendTestNS2_ZendSubNS_namespaced_func, 0, 0, _IS_BOOL, 0)
138++ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_zend_get_map_ptr_last, 0, 0, IS_LONG, 0)
139+ ZEND_END_ARG_INFO()
140+
141+-ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class__ZendTestClass_is_object, 0, 0, IS_LONG, 0)
142++ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_ZendTestNS2_ZendSubNS_namespaced_func, 0, 0, _IS_BOOL, 0)
143+ ZEND_END_ARG_INFO()
144+
145++#define arginfo_class__ZendTestClass_is_object arginfo_zend_get_map_ptr_last
146++
147+ ZEND_BEGIN_ARG_INFO_EX(arginfo_class__ZendTestClass___toString, 0, 0, 0)
148+ ZEND_END_ARG_INFO()
149+
150+@@ -109,6 +111,7 @@
151+ static ZEND_FUNCTION(zend_weakmap_remove);
152+ static ZEND_FUNCTION(zend_weakmap_dump);
153+ static ZEND_FUNCTION(zend_get_unit_enum);
154++static ZEND_FUNCTION(zend_get_map_ptr_last);
155+ static ZEND_FUNCTION(namespaced_func);
156+ static ZEND_METHOD(_ZendTestClass, is_object);
157+ static ZEND_METHOD(_ZendTestClass, __toString);
158+@@ -139,6 +142,7 @@
159+ ZEND_FE(zend_weakmap_remove, arginfo_zend_weakmap_remove)
160+ ZEND_FE(zend_weakmap_dump, arginfo_zend_weakmap_dump)
161+ ZEND_FE(zend_get_unit_enum, arginfo_zend_get_unit_enum)
162++ ZEND_FE(zend_get_map_ptr_last, arginfo_zend_get_map_ptr_last)
163+ ZEND_NS_FE("ZendTestNS2\\ZendSubNS", namespaced_func, arginfo_ZendTestNS2_ZendSubNS_namespaced_func)
164+ ZEND_FE_END
165+ };
166+--- /dev/null
167++++ b/sapi/fpm/tests/gh8646.phpt
168+@@ -0,0 +1,52 @@
169++--TEST--
170++GH-8646 (Memory leak PHP FPM 8.1)
171++--EXTENSIONS--
172++zend_test
173++--SKIPIF--
174++<?php include "skipif.inc"; ?>
175++--FILE--
176++<?php
177++
178++require_once "tester.inc";
179++
180++$cfg = <<<EOT
181++[global]
182++error_log = {{FILE:LOG}}
183++[unconfined]
184++listen = {{ADDR}}
185++pm = dynamic
186++pm.max_children = 5
187++pm.start_servers = 1
188++pm.min_spare_servers = 1
189++pm.max_spare_servers = 3
190++EOT;
191++
192++$code = <<<EOT
193++<?php
194++class MyClass {}
195++echo zend_get_map_ptr_last();
196++EOT;
197++
198++$tester = new FPM\Tester($cfg, $code);
199++$tester->start();
200++$tester->expectLogStartNotices();
201++$map_ptr_last_values = [];
202++for ($i = 0; $i < 10; $i++) {
203++ $map_ptr_last_values[] = (int) $tester->request()->getBody();
204++}
205++// Ensure that map_ptr_last did not increase
206++var_dump(count(array_unique($map_ptr_last_values, SORT_REGULAR)) === 1);
207++$tester->terminate();
208++$tester->expectLogTerminatingNotices();
209++$tester->close();
210++
211++?>
212++Done
213++--EXPECT--
214++bool(true)
215++Done
216++--CLEAN--
217++<?php
218++require_once "tester.inc";
219++FPM\Tester::clean();
220++?>
221diff --git a/debian/patches/series b/debian/patches/series
222index 3cdd5eb..afae379 100644
223--- a/debian/patches/series
224+++ b/debian/patches/series
225@@ -63,3 +63,4 @@ CVE-2023-0567-2.patch
226 CVE-2023-0568.patch
227 CVE-2023-0662-1.patch
228 CVE-2023-0662-2.patch
229+fix-map-ptr-mem-leak.patch

Subscribers

People subscribed via source and target branches