diff --git a/.github/actions/apt-x32/action.yml b/.github/actions/apt-x32/action.yml index a70ff03e9c90..484d252ab333 100644 --- a/.github/actions/apt-x32/action.yml +++ b/.github/actions/apt-x32/action.yml @@ -9,10 +9,10 @@ runs: OPCACHE_TLS_TESTS_DEPS="gcc clang lld" export DEBIAN_FRONTEND=noninteractive - dpkg --add-architecture i386 - apt-get update -y | true + sudo dpkg --add-architecture i386 + sudo env DEBIAN_FRONTEND=noninteractive apt-get update -y | true # TODO: Reenable postgresql + postgresql-contrib packages once they work again. - apt-get install -y \ + sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y \ autoconf \ bison \ curl \ diff --git a/.github/actions/install-linux-x32/action.yml b/.github/actions/install-linux-x32/action.yml index 4ef87ee03fd3..95d7fc738b1c 100644 --- a/.github/actions/install-linux-x32/action.yml +++ b/.github/actions/install-linux-x32/action.yml @@ -5,11 +5,11 @@ runs: - shell: bash run: | set -x - make install - mkdir -p /etc/php.d - chmod 777 /etc/php.d - echo mysqli.default_socket=/var/run/mysqld/mysqld.sock > /etc/php.d/mysqli.ini - echo pdo_mysql.default_socket=/var/run/mysqld/mysqld.sock > /etc/php.d/pdo_mysql.ini - echo opcache.enable_cli=1 >> /etc/php.d/opcache.ini - echo opcache.protect_memory=1 >> /etc/php.d/opcache.ini - echo opcache.preload_user=root >> /etc/php.d/opcache.ini + sudo make install + sudo mkdir -p /etc/php.d + sudo chmod 777 /etc/php.d + echo mysqli.default_socket=/var/run/mysqld/mysqld.sock | sudo tee /etc/php.d/mysqli.ini > /dev/null + echo pdo_mysql.default_socket=/var/run/mysqld/mysqld.sock | sudo tee /etc/php.d/pdo_mysql.ini > /dev/null + echo opcache.enable_cli=1 | sudo tee /etc/php.d/opcache.ini > /dev/null + echo opcache.protect_memory=1 | sudo tee -a /etc/php.d/opcache.ini > /dev/null + echo opcache.preload_user=root | sudo tee -a /etc/php.d/opcache.ini > /dev/null diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index dcd61adf17c8..51c8e4c673c1 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -230,15 +230,14 @@ jobs: fail-fast: false matrix: ${{ fromJson(inputs.branch).jobs.LINUX_X32.matrix }} name: "LINUX_X32_${{ matrix.debug && 'DEBUG' || 'RELEASE' }}_${{ matrix.zts && 'ZTS' || 'NTS' }}" - runs-on: ubuntu-latest + # The container seccomp profile blocks i386 socket syscalls used by the test suite. + runs-on: ubuntu-${{ fromJson(inputs.branch).config.ubuntu_version }} timeout-minutes: 180 - container: - image: ubuntu:${{ fromJson(inputs.branch).config.ubuntu_version }} env: - MYSQL_TEST_HOST: mysql - PDO_MYSQL_TEST_DSN: mysql:host=mysql;dbname=test - PDO_MYSQL_TEST_HOST: mysql - PDO_FIREBIRD_TEST_DSN: firebird:dbname=firebird:test.fdb + MYSQL_TEST_HOST: 127.0.0.1 + PDO_MYSQL_TEST_DSN: mysql:host=127.0.0.1;dbname=test + PDO_MYSQL_TEST_HOST: 127.0.0.1 + PDO_FIREBIRD_TEST_DSN: firebird:dbname=localhost:test.fdb services: mysql: image: mysql:8.4 diff --git a/NEWS b/NEWS index 24432565e6e0..e437d34b1e30 100644 --- a/NEWS +++ b/NEWS @@ -3,6 +3,8 @@ PHP NEWS ?? ??? ????, PHP 8.4.28 - Core: + . Garbage collection can complete wide graph traversals after the request + memory limit has been reached. (Alessio Attilio) . Fixed bug GH-24184 (System ID not being reinitialized after an Apache graceful restart, potentially causing incompatible OPcache file cache entries to be reused). (Levi Morrison) diff --git a/Zend/tests/gh23586_002.phpt b/Zend/tests/gh23586_002.phpt new file mode 100644 index 000000000000..9c5d799da7ce --- /dev/null +++ b/Zend/tests/gh23586_002.phpt @@ -0,0 +1,23 @@ +--TEST-- +GH-23586 (GC can finish a wide traversal at the memory limit) +--SKIPIF-- + +--INI-- +memory_limit=256M +--FILE-- +children[] = (object) ['parent' => $root]; +} +unset($root); +ini_set('memory_limit', (string) memory_get_usage(true)); +var_dump(gc_collect_cycles()); +?> +--EXPECT-- +int(350002) diff --git a/Zend/tests/gh23586_003.phpt b/Zend/tests/gh23586_003.phpt new file mode 100644 index 000000000000..106df58342f7 --- /dev/null +++ b/Zend/tests/gh23586_003.phpt @@ -0,0 +1,30 @@ +--TEST-- +GH-23586 (GC traversal storage is released when a destructor bails out) +--SKIPIF-- + +--INI-- +error_reporting=E_ALL & ~E_DEPRECATED +--FILE-- +children[] = (object) ['parent' => $root]; +} +unset($root); +gc_collect_cycles(); +?> +--EXPECTF-- +Fatal error: Destructor bailout in %s on line %d diff --git a/Zend/zend_gc.c b/Zend/zend_gc.c index eb54ead7b110..5e23fc0bd225 100644 --- a/Zend/zend_gc.c +++ b/Zend/zend_gc.c @@ -333,7 +333,8 @@ struct _gc_stack { static zend_never_inline gc_stack* gc_stack_next(gc_stack *stack) { if (UNEXPECTED(!stack->next)) { - gc_stack *segment = emalloc(sizeof(gc_stack)); + /* Traversal must not bail out on the request memory limit after changing refcounts. */ + gc_stack *segment = pemalloc(sizeof(gc_stack), true); segment->prev = stack; segment->next = NULL; stack->next = segment; @@ -371,7 +372,7 @@ static void gc_stack_free(gc_stack *stack) while (p) { stack = p->next; - efree(p); + pefree(p, true); p = stack; } } @@ -1943,7 +1944,7 @@ static zend_never_inline void gc_call_destructors_in_fiber(uint32_t end) } } -ZEND_API int zend_gc_collect_cycles(void) +static int zend_gc_collect_cycles_impl(gc_stack *stack) { int total_count = 0; bool should_rerun_gc = 0; @@ -1961,10 +1962,6 @@ ZEND_API int zend_gc_collect_cycles(void) zend_refcounted *p; uint32_t gc_flags = 0; uint32_t idx, end; - gc_stack stack; - - stack.prev = NULL; - stack.next = NULL; if (GC_G(gc_active)) { GC_G(collector_time) += zend_hrtime() - start_time; @@ -1976,17 +1973,16 @@ ZEND_API int zend_gc_collect_cycles(void) GC_G(gc_active) = 1; GC_TRACE("Marking roots"); - gc_mark_roots(&stack); + gc_mark_roots(stack); GC_TRACE("Scanning roots"); - gc_scan_roots(&stack); + gc_scan_roots(stack); GC_TRACE("Collecting roots"); - count = gc_collect_roots(&gc_flags, &stack); + count = gc_collect_roots(&gc_flags, stack); if (!GC_G(num_roots)) { /* nothing to free */ GC_TRACE("Nothing to free"); - gc_stack_free(&stack); GC_G(gc_active) = 0; goto finish; } @@ -2035,7 +2031,7 @@ ZEND_API int zend_gc_collect_cycles(void) while (idx != end) { if (GC_IS_DTOR_GARBAGE(current->ref)) { p = GC_GET_PTR(current->ref); - count -= gc_remove_nested_data_from_buffer(p, current, &stack); + count -= gc_remove_nested_data_from_buffer(p, current, stack); } current++; idx++; @@ -2058,8 +2054,6 @@ ZEND_API int zend_gc_collect_cycles(void) } } - gc_stack_free(&stack); - /* Destroy zvals. The root buffer may be reallocated. */ GC_TRACE("Destroying zvals"); zend_hrtime_t free_start_time = zend_hrtime(); @@ -2144,6 +2138,32 @@ ZEND_API int zend_gc_collect_cycles(void) return total_count; } +static int zend_gc_collect_cycles_with_stack(gc_stack *stack) +{ + int count; + + zend_try { + count = zend_gc_collect_cycles_impl(stack); + } zend_catch { + gc_stack_free(stack); + zend_bailout(); + } zend_end_try(); + + gc_stack_free(stack); + return count; +} + +ZEND_API int zend_gc_collect_cycles(void) +{ + /* Keep the modified stack outside the function containing setjmp(), so it remains + * valid when freeing traversal storage after a bailout. */ + gc_stack stack; + + stack.prev = NULL; + stack.next = NULL; + return zend_gc_collect_cycles_with_stack(&stack); +} + ZEND_API void zend_gc_get_status(zend_gc_status *status) { status->active = GC_G(gc_active);