Skip to content

Commit 51faf04

Browse files
committed
Fix phpGH-10737: PHP 8.1.16 segfaults on line 597 of sapi/apache2handler/sapi_apache2.c
The TSRM keeps a hashtable mapping the thread IDs to the thread resource pointers. It's possible that the thread disappears without us knowing, and then another thread gets spawned some time later with the same ID as the disappeared thread. Note that since it's a new thread the TSRM key pointer and cached pointer will be NULL. The Apache request handler `php_handler()` will try to fetch some fields from the SAPI globals. It uses a lazy thread resource allocation by calling `ts_resource(0);`. This allocates a thread resource and sets up the TSRM pointers if they haven't been set up yet. At least, that's what's supposed to happen. But since we are in a situation where the thread ID still has the resources of the *old* thread associated in the hashtable, the loop in `ts_resource_ex` will find that thread resource and assume the thread has been setup already. But this is not the case since this thread is actually a new thread, just reusing the ID of the old one, without any relation whatsoever to the old thread. Because of this assumption, the TSRM pointers will not be setup, leading to a NULL pointer dereference when trying to access the SAPI globals. We can easily detect this scenario: if we're in the fallback path, and the pointer is NULL, and we're looking for our own thread resource, we know we're actually reusing a thread ID. In that case, we'll free up the old thread resources gracefully (gracefully because there might still be resources open like database connection which need to be shut down cleanly). After freeing the resources, we'll create the new resources for this thread as if the stale resources never existed in the first place. From that point forward, it is as if that situation never occurred. The fact that this situation happens isn't that bad because a child process containing threads will eventually be respawned anyway by the SAPI, so the stale thread resources won't remain forever. Note that we can't simply assign our own TSRM pointers to the existing thread resource for our ID, since it was actually from a different thread (just with the same ID!). Furthermore, the dynamically loaded extensions have their own pointer, which is only set when their constructor is called, so we'd have to call their constructor anyway... I also tried to call the dtor and then the ctor again for those resources on the pre-existing thread resource to reuse storage, but that didn't work properly because other code doesn't expect something like that to happen, which breaks assumptions, and this in turn caused Valgrind to (rightfully) complain about memory bugs. Note 2: I also had to fix a bug in the core globals destruction because it always assumed that the thread destroying them was the owning thread, which on TSRM shutdown isn't always the case. A similar bug was fixed recently with the JIT globals. Closes phpGH-10863.
1 parent ebb3213 commit 51faf04

File tree

5 files changed

+76
-48
lines changed

5 files changed

+76
-48
lines changed

NEWS

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ PHP NEWS
88
(nielsdos)
99
. Fixed bug GH-10085 (Assertion when adding two arrays with += where the first
1010
array is contained in the second). (ilutov)
11+
. Fixed bug GH-10737 (PHP 8.1.16 segfaults on line 597 of
12+
sapi/apache2handler/sapi_apache2.c). (nielsdos)
1113

1214
- DOM:
1315
. Fixed bug #80602 (Segfault when using DOMChildNode::before()).

TSRM/TSRM.c

+70-44
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,23 @@ TSRM_API int tsrm_startup(int expected_threads, int expected_resources, int debu
161161
return 1;
162162
}/*}}}*/
163163

164+
static void ts_free_resources(tsrm_tls_entry *thread_resources)
165+
{
166+
/* Need to destroy in reverse order to respect dependencies. */
167+
for (int i = thread_resources->count - 1; i >= 0; i--) {
168+
if (!resource_types_table[i].done) {
169+
if (resource_types_table[i].dtor) {
170+
resource_types_table[i].dtor(thread_resources->storage[i]);
171+
}
172+
173+
if (!resource_types_table[i].fast_offset) {
174+
free(thread_resources->storage[i]);
175+
}
176+
}
177+
}
178+
179+
free(thread_resources->storage);
180+
}
164181

165182
/* Shutdown TSRM (call once for the entire process) */
166183
TSRM_API void tsrm_shutdown(void)
@@ -183,25 +200,13 @@ TSRM_API void tsrm_shutdown(void)
183200
tsrm_tls_entry *p = tsrm_tls_table[i], *next_p;
184201

185202
while (p) {
186-
int j;
187-
188203
next_p = p->next;
189-
for (j=0; j<p->count; j++) {
190-
if (p->storage[j]) {
191-
if (resource_types_table) {
192-
if (!resource_types_table[j].done) {
193-
if (resource_types_table[j].dtor) {
194-
resource_types_table[j].dtor(p->storage[j]);
195-
}
196-
197-
if (!resource_types_table[j].fast_offset) {
198-
free(p->storage[j]);
199-
}
200-
}
201-
}
202-
}
204+
if (resource_types_table) {
205+
/* This call will already free p->storage for us */
206+
ts_free_resources(p);
207+
} else {
208+
free(p->storage);
203209
}
204-
free(p->storage);
205210
free(p);
206211
p = next_p;
207212
}
@@ -367,7 +372,13 @@ TSRM_API ts_rsrc_id ts_allocate_fast_id(ts_rsrc_id *rsrc_id, size_t *offset, siz
367372
return *rsrc_id;
368373
}/*}}}*/
369374

375+
static void set_thread_local_storage_resource_to(tsrm_tls_entry *thread_resource)
376+
{
377+
tsrm_tls_set(thread_resource);
378+
TSRMLS_CACHE = thread_resource;
379+
}
370380

381+
/* Must be called with tsmm_mutex held */
371382
static void allocate_new_resource(tsrm_tls_entry **thread_resources_ptr, THREAD_T thread_id)
372383
{/*{{{*/
373384
int i;
@@ -383,8 +394,7 @@ static void allocate_new_resource(tsrm_tls_entry **thread_resources_ptr, THREAD_
383394
(*thread_resources_ptr)->next = NULL;
384395

385396
/* Set thread local storage to this new thread resources structure */
386-
tsrm_tls_set(*thread_resources_ptr);
387-
TSRMLS_CACHE = *thread_resources_ptr;
397+
set_thread_local_storage_resource_to(*thread_resources_ptr);
388398

389399
if (tsrm_new_thread_begin_handler) {
390400
tsrm_new_thread_begin_handler(thread_id);
@@ -407,17 +417,14 @@ static void allocate_new_resource(tsrm_tls_entry **thread_resources_ptr, THREAD_
407417
if (tsrm_new_thread_end_handler) {
408418
tsrm_new_thread_end_handler(thread_id);
409419
}
410-
411-
tsrm_mutex_unlock(tsmm_mutex);
412420
}/*}}}*/
413421

414-
415422
/* fetches the requested resource for the current thread */
416423
TSRM_API void *ts_resource_ex(ts_rsrc_id id, THREAD_T *th_id)
417424
{/*{{{*/
418425
THREAD_T thread_id;
419426
int hash_value;
420-
tsrm_tls_entry *thread_resources;
427+
tsrm_tls_entry *thread_resources, **last_thread_resources;
421428

422429
if (!th_id) {
423430
/* Fast path for looking up the resources for the current
@@ -448,25 +455,55 @@ TSRM_API void *ts_resource_ex(ts_rsrc_id id, THREAD_T *th_id)
448455

449456
if (!thread_resources) {
450457
allocate_new_resource(&tsrm_tls_table[hash_value], thread_id);
458+
tsrm_mutex_unlock(tsmm_mutex);
451459
return ts_resource_ex(id, &thread_id);
452460
} else {
453-
do {
454-
if (thread_resources->thread_id == thread_id) {
455-
break;
456-
}
461+
last_thread_resources = &tsrm_tls_table[hash_value];
462+
while (thread_resources->thread_id != thread_id) {
463+
last_thread_resources = &thread_resources->next;
457464
if (thread_resources->next) {
458465
thread_resources = thread_resources->next;
459466
} else {
460467
allocate_new_resource(&thread_resources->next, thread_id);
468+
tsrm_mutex_unlock(tsmm_mutex);
461469
return ts_resource_ex(id, &thread_id);
462-
/*
463-
* thread_resources = thread_resources->next;
464-
* break;
465-
*/
466470
}
467-
} while (thread_resources);
471+
}
472+
}
473+
474+
/* It's possible that the current thread resources are requested, and that we get here.
475+
* This means that the TSRM key pointer and cached pointer are NULL, but there is still
476+
* a thread resource associated with this ID in the hashtable. This can occur if a thread
477+
* goes away, but its resources are never cleaned up, and then that thread ID is reused.
478+
* Since we don't always have a way to know when a thread goes away, we can't clean up
479+
* the thread's resources before the new thread spawns.
480+
* To solve this issue, we'll free up the old thread resources gracefully (gracefully
481+
* because there might still be resources open like database connection which need to
482+
* be shut down cleanly). After freeing up, we'll create the new resources for this thread
483+
* as if the stale resources never existed in the first place. From that point forward,
484+
* it is as if that situation never occurred.
485+
* The fact that this situation happens isn't that bad because a child process containing
486+
* threads will eventually be respawned anyway by the SAPI, so the stale threads won't last
487+
* forever. */
488+
TSRM_ASSERT(thread_resources->thread_id == thread_id);
489+
if (thread_id == tsrm_thread_id() && !tsrm_tls_get()) {
490+
tsrm_tls_entry *next = thread_resources->next;
491+
/* In case that extensions don't use the pointer passed from the dtor, but incorrectly
492+
* use the global pointer, we need to setup the global pointer temporarily here. */
493+
set_thread_local_storage_resource_to(thread_resources);
494+
/* Free up the old resource from the old thread instance */
495+
ts_free_resources(thread_resources);
496+
free(thread_resources);
497+
/* Allocate a new resource at the same point in the linked list, and relink the next pointer */
498+
allocate_new_resource(last_thread_resources, thread_id);
499+
thread_resources = *last_thread_resources;
500+
thread_resources->next = next;
501+
/* We don't have to tail-call ts_resource_ex, we can take the fast path to the return
502+
* because we already have the correct pointer. */
468503
}
504+
469505
tsrm_mutex_unlock(tsmm_mutex);
506+
470507
/* Read a specific resource from the thread's resources.
471508
* This is called outside of a mutex, so have to be aware about external
472509
* changes to the structure as we read it.
@@ -479,7 +516,6 @@ TSRM_API void *ts_resource_ex(ts_rsrc_id id, THREAD_T *th_id)
479516
void ts_free_thread(void)
480517
{/*{{{*/
481518
tsrm_tls_entry *thread_resources;
482-
int i;
483519
THREAD_T thread_id = tsrm_thread_id();
484520
int hash_value;
485521
tsrm_tls_entry *last=NULL;
@@ -492,17 +528,7 @@ void ts_free_thread(void)
492528

493529
while (thread_resources) {
494530
if (thread_resources->thread_id == thread_id) {
495-
for (i=0; i<thread_resources->count; i++) {
496-
if (resource_types_table[i].dtor) {
497-
resource_types_table[i].dtor(thread_resources->storage[i]);
498-
}
499-
}
500-
for (i=0; i<thread_resources->count; i++) {
501-
if (!resource_types_table[i].fast_offset) {
502-
free(thread_resources->storage[i]);
503-
}
504-
}
505-
free(thread_resources->storage);
531+
ts_free_resources(thread_resources);
506532
if (last) {
507533
last->next = thread_resources->next;
508534
} else {

main/main.c

+1-1
Original file line numberDiff line numberDiff line change
@@ -1936,7 +1936,7 @@ static void core_globals_dtor(php_core_globals *core_globals)
19361936
free(core_globals->php_binary);
19371937
}
19381938

1939-
php_shutdown_ticks();
1939+
php_shutdown_ticks(core_globals);
19401940
}
19411941
/* }}} */
19421942

main/php_ticks.c

+2-2
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@ void php_deactivate_ticks(void)
3434
zend_llist_clean(&PG(tick_functions));
3535
}
3636

37-
void php_shutdown_ticks(void)
37+
void php_shutdown_ticks(php_core_globals *core_globals)
3838
{
39-
zend_llist_destroy(&PG(tick_functions));
39+
zend_llist_destroy(&core_globals->tick_functions);
4040
}
4141

4242
static int php_compare_tick_functions(void *elem1, void *elem2)

main/php_ticks.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
int php_startup_ticks(void);
2121
void php_deactivate_ticks(void);
22-
void php_shutdown_ticks(void);
22+
void php_shutdown_ticks(php_core_globals *core_globals);
2323
void php_run_ticks(int count);
2424

2525
BEGIN_EXTERN_C()

0 commit comments

Comments
 (0)