PHP 8.4 is the default PHP on openSUSE Leap 16.0. You can install PHP 8.4 on openSUSE Leap 16 straight from the main OSS repository with no extra repo. Leap 15.6 ships PHP 8.2 instead, and the devel:languages:php OBS repository that older guides point at now hands you PHP 8.5, so on 15.6 you add the project’s php84 subrepository to land on the 8.4 branch.
Below we install PHP 8.4 with the common extensions, serve it through Apache with mod_php and through Nginx with PHP-FPM, and check that the build is patched against PHP2Shell. The install, Apache and Nginx steps were run on fresh openSUSE Leap 16.0 and Leap 15.6 systems in October 2026 and ended with PHP 8.4.27 answering web requests on both, while the firewall, AppArmor and SELinux notes were not re-tested.
What PHP 8.4 adds
The examples below come from the PHP 8.4.0 release notes, each shown the old way first and then the 8.4 way, so you can see what changes in your own code.
Property hooks and asymmetric visibility. Property hooks give you computed properties that IDEs and static analysis tools understand natively, so docblock comments no longer drift out of sync, and you can pre- or post-process a value without a matching getter or setter. Asymmetric visibility controls reading and writing a property separately, which removes the boilerplate getter you used to write just to expose a value read-only. A class written for earlier versions:
# In earlier versions
class PhpVersion
{
private string $version = '8.3';
public function getVersion(): string
{
return $this->version;
}
public function increment(): void
{
[$major, $minor] = explode('.', $this->version);
$minor++;
$this->version = "{$major}.{$minor}";
}
}
With public private(set) the getter disappears, because outside code can read the property but only the class can write it:
# In PHP 8.4 versions
class PhpVersion
{
public private(set) string $version = '8.4';
public function increment(): void
{
[$major, $minor] = explode('.', $this->version);
$minor++;
$this->version = "{$major}.{$minor}";
}
}
The #[\Deprecated] attribute. PHP’s own deprecation mechanism is now available to user-defined functions, methods and class constants. Before 8.4 a docblock was the only marker, and callers got no warning at runtime:
#In earlier PHP versions
class PhpVersion
{
/**
* @deprecated 8.3 use PhpVersion::getVersion() instead
*/
public function getPhpVersion(): string
{
return $this->getVersion();
}
public function getVersion(): string
{
return '8.3';
}
}
$phpVersion = new PhpVersion();
// No indication that the method is deprecated.
echo $phpVersion->getPhpVersion();
The attribute makes the engine emit a real deprecation notice when the method is called:
#In PHP 8.4 versions
class PhpVersion
{
#[\Deprecated(
message: "use PhpVersion::getVersion() instead",
since: "8.4",
)]
public function getPhpVersion(): string
{
return $this->getVersion();
}
public function getVersion(): string
{
return '8.4';
}
}
$phpVersion = new PhpVersion();
// Deprecated: Method PhpVersion::getPhpVersion() is deprecated since 8.4, use PhpVersion::getVersion() instead
echo $phpVersion->getPhpVersion();
New ext-dom features and HTML5 support. The new DOM API fixes long-standing compliance flaws, parses HTML5 documents in a standards-compliant way and adds CSS selectors, so the XPath gymnastics of the old API shrink to one querySelector() call:
# In earlier PHP versions
$dom = new DOMDocument();
$dom->loadHTML(
<<<'HTML'
<main>
<article>PHP 8.4 is a feature-rich release!</article>
<article class="featured">PHP 8.4 adds new DOM classes that are spec-compliant, keeping the old ones for compatibility.</article>
</main>
HTML,
LIBXML_NOERROR,
);
$xpath = new DOMXPath($dom);
$node = $xpath->query(".//main/article[not(following-sibling::*)]")[0];
$classes = explode(" ", $node->className); // Simplified
var_dump(in_array("featured", $classes)); // bool(true)
# In PHP 8.4
$dom = Dom\HTMLDocument::createFromString(
<<<'HTML'
<main>
<article>PHP 8.4 is a feature-rich release!</article>
<article class="featured">PHP 8.4 adds new DOM classes that are spec-compliant, keeping the old ones for compatibility.</article>
</main>
HTML,
LIBXML_NOERROR,
);
$node = $dom->querySelector('main > article:last-child');
var_dump($node->classList->contains("featured")); // bool(true)
Object API for BCMath. The new BcMath\Number object lets arbitrary precision numbers use the standard mathematical operators. The objects are immutable and implement Stringable, so echo $num works as you’d expect:
#In earlier PHP versions
$num1 = '0.12345';
$num2 = 2;
$result = bcadd($num1, $num2, 5);
echo $result; // '2.12345'
var_dump(bccomp($num1, $num2) > 0); // false
#In PHP Version 8.4
use BcMath\Number;
$num1 = new Number('0.12345');
$num2 = new Number('2');
$result = $num1 + $num2;
echo $result; // '2.12345'
var_dump($num1 > $num2); // false
New array_*() functions. array_find(), array_find_key(), array_any() and array_all() replace the foreach-and-break loops most codebases carry:
#In earlier PHP versions
$animal = null;
foreach (['dog', 'cat', 'cow', 'duck', 'goose'] as $value) {
if (str_starts_with($value, 'c')) {
$animal = $value;
break;
}
}
var_dump($animal); // string(3) "cat"
#In PHP 8.4
$animal = array_find(
['dog', 'cat', 'cow', 'duck', 'goose'],
static fn (string $value): bool => str_starts_with($value, 'c'),
);
var_dump($animal); // string(3) "cat"
new MyClass()->method() without parentheses. You can now call methods and read properties on a freshly created object without wrapping the new expression in parentheses:
#In earlier PHP versions
class PhpVersion
{
public function getVersion(): string
{
return 'PHP 8.3';
}
}
var_dump((new PhpVersion())->getVersion());
#In PHP 8.4
class PhpVersion
{
public function getVersion(): string
{
return 'PHP 8.4';
}
}
var_dump(new PhpVersion()->getVersion());
None of these changes need a php.ini switch. They are on as soon as the 8.4 interpreter runs your code, which is what the rest of this guide sets up.
Update the system
Bring the system up to date before adding anything, because PHP’s dependencies (ICU, libgd, OpenSSL) come from the base repositories and a stale package cache is the usual cause of resolver conflicts later:
sudo zypper refresh
sudo zypper update -y
Then check which openSUSE release you are on, since the repository step differs between Leap 16.0 and Leap 15.6:
cat /etc/os-release
Our Leap 16.0 box reported:
NAME="openSUSE Leap"
VERSION="16.0"
ID="opensuse-leap"
ID_LIKE="suse opensuse"
VERSION_ID="16.0"
PRETTY_NAME="openSUSE Leap 16.0"
ANSI_COLOR="0;32"
CPE_NAME="cpe:/o:opensuse:leap:16.0"
BUG_REPORT_URL="https://bugs.opensuse.org"
HOME_URL="https://www.opensuse.org/"
DOCUMENTATION_URL="https://en.opensuse.org/Portal:Leap"
LOGO="distributor-logo-Leap"
On Leap 16.0 skip the next section: the main OSS repository already carries PHP 8.4, and an extra repository would only add a second source for the same packages.
Add the PHP 8.4 repository on Leap 15.6
Leap 15.6’s own php8 package is PHP 8.2, so you need the devel:languages:php:php84 OBS repository to get 8.4. Don’t use the parent devel:languages:php repository for this: it moved to PHP 8.5 and now offers 8.5.12 as the php8 package. Leap 15.6 also reached end of life on 30 April 2026 and its base packages no longer receive security updates, so plan the upgrade to Leap 16 while the PHP repository keeps you on a patched 8.4 in the meantime.
sudo zypper addrepo https://download.opensuse.org/repositories/devel:languages:php:php84/openSUSE_Leap_15.6/devel:languages:php:php84.repo
sudo zypper modifyrepo --refresh devel_languages_php_php84
sudo zypper refresh
The modifyrepo line turns on autorefresh, which addrepo leaves off, so later zypper update runs see new builds. The refresh then asks you to trust the repository signing key and prints its details:
Repository: php 8.4 (openSUSE_Leap_15.6)
Key Fingerprint: E1E2 F102 BC77 314F 4A4B 7542 BED0 FF75 7D17 C956
Key Name: devel:languages:php OBS Project <devel:languages:[email protected]>
Key Algorithm: RSA 4096
Key Created: Sat Jun 27 09:08:06 2026
Key Expires: Mon Sep 4 09:08:06 2028
Rpm Name: gpg-pubkey-7d17c956-6a3f92f6
Compare that fingerprint with the key published next to the repository metadata before you answer a to trust it always:
curl -fsSL https://download.opensuse.org/repositories/devel:/languages:/php:/php84/openSUSE_Leap_15.6/repodata/repomd.xml.key | gpg --show-keys
The fingerprint on the second line must match the prompt. The date gpg prints (2024-05-02) is the key’s original creation date, while zypper’s “Key Created” shows the last re-signing, so compare only the fingerprint:
pub rsa4096 2024-05-02 [SC] [expires: 2028-09-04]
E1E2F102BC77314F4A4B7542BED0FF757D17C956
uid devel:languages:php OBS Project <devel:languages:[email protected]>
The same OBS project publishes trees for SUSE Linux Enterprise 15 SP6 and SP7 under the same URL pattern. We only ran the Leap 15.6 tree, so on SLES pick the matching directory from the repository listing and verify the result with rpm -q php8 after the install.
Install PHP 8.4
The package is called php8 on every Leap release, and the repository decides which 8.x branch you get. Install it:
sudo zypper install php8
Accept the prompt. On Leap 16.0 the recommended packages include postfix and perl, which surprises people who expected a small CLI install, so read the list before you answer y:
Resolving package dependencies...
The following 9 recommended packages were automatically selected:
php8-ctype php8-dom php8-iconv php8-openssl php8-sqlite php8-tokenizer php8-xmlreader php8-xmlwriter postfix
The following 4 packages are suggested, but will not be installed:
php8-gd php8-gettext php8-mbstring php8-mysql
The following 28 NEW packages are going to be installed:
ed iproute2 libargon2-1 libbpf1 libelf1 libgdbm6 libgdbm_compat4 libicu77 libicu77-ledata liblmdb-0_9_30 libmnl0 libxtables12 perl perl-base php8 php8-cli php8-ctype php8-dom php8-iconv php8-openssl php8-pdo php8-sqlite php8-tokenizer php8-xmlreader php8-xmlwriter postfix system-user-mail system-user-wwwrun
28 new packages to install.
Package download size: 29.5 MiB
Package install size change:
| 111.8 MiB required by packages that will be installed
111.8 MiB | - 0 B released by packages that will be removed
Backend: classic_rpmtrans
Continue? [y/n/v/...? shows all options] (y): y
Extensions follow the php8-<extension> naming, so zypper search php8- lists everything available:
sudo zypper install php8-<extension>
For a typical web application, install the zip, GD, gettext, mbstring and MySQL extensions together with PHP-FPM:
sudo zypper install php8-zip php8-fpm php8-gd php8-gettext php8-mbstring php8-mysql
Accept the installation prompt:
Resolving package dependencies...
The following 28 NEW packages are going to be installed:
dbus-1-x11 fontconfig libX11-6 libX11-data libXau6 libXpm4 libfontconfig1 libfreetype6 libgd3 libgnutls30 libhogweed6 libjbig2 libjpeg8 libleancrypto1 libnettle8 libonig5 libpng16-16 libsharpyuv0 libtiff6 libwebp7 libxcb1 libzip5 php8-fpm php8-gd php8-gettext php8-mbstring php8-mysql php8-zip
28 new packages to install.
Package download size: 10.5 MiB
Package install size change:
| 26.3 MiB required by packages that will be installed
26.3 MiB | - 0 B released by packages that will be removed
Backend: classic_rpmtrans
Continue? [y/n/v/...? shows all options] (y): y
Confirm the PHP version:
php --version
Leap 16.0 reports the build from its main repository. The block below is the Leap 16.0 capture. The Leap 15.6 box with the php84 repository printed the same version with built: Oct 21 2026 12:00:00 instead:
PHP 8.4.27 (cli) (built: Oct 20 2026 11:25:46) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.4.27, Copyright (c) Zend Technologies
List the loaded modules:
php -m
Every extension installed above shows up, along with the recommended ones zypper pulled in (ctype, dom, iconv, openssl, sqlite and the XML readers):
[PHP Modules]
Core
ctype
date
dom
filter
gd
gettext
hash
iconv
json
libxml
mbstring
mysqli
mysqlnd
openssl
pcre
PDO
pdo_mysql
pdo_sqlite
random
Reflection
session
SimpleXML
SPL
sqlite3
standard
tokenizer
xml
xmlreader
xmlwriter
zip
[Zend Modules]
If mbstring or zip is missing, the second install command didn’t complete, so rerun it before configuring a web server. The PHP2Shell check for this build comes after the web server sections, once there is a running service to verify.
Run PHP 8.4 with Apache
Apache with mod_php is the shortest path to a working PHP page. Install the apache2-mod_php8 package, which pulls in Apache with the prefork MPM:
sudo zypper in apache2-mod_php8
Load the PHP Apache module. On both Leap releases the package had already enabled it, so a2enmod answered "php8" already present, which is fine:
sudo a2enmod php8
Then start and enable the apache2 service:
sudo systemctl restart apache2
sudo systemctl enable apache2
Create a test page in the default document root, /srv/www/htdocs. It prints the PHP version and the server API, so you can see which handler answered:
echo '<?php echo "PHP Hello Page, PHP " . PHP_VERSION . " via " . PHP_SAPI . PHP_EOL;' | sudo tee /srv/www/htdocs/index.php
curl -s http://localhost/index.php
The apache2handler API confirms mod_php served the page:
PHP Hello Page, PHP 8.4.27 via apache2handler
From another machine, open http://10.0.1.50/index.php with your server’s address. If the browser times out while the local curl works, the firewall is blocking port 80. The Nginx on Leap 16 guide covers the firewalld rule, and the same http service applies to Apache.
Run PHP 8.4 with Nginx and PHP-FPM
Nginx can’t run PHP itself, so it hands .php requests to PHP-FPM over FastCGI. Both servers want port 80, so stop and disable apache2 first:
sudo systemctl disable --now apache2
Install Nginx and start it:
sudo zypper install nginx
sudo systemctl enable --now nginx
Install the php8-fpm package if you skipped it in the extensions step:
sudo zypper install php8-fpm
Open the main FPM configuration file:
sudo vim /etc/php8/fpm/php-fpm.conf
Uncomment the error log line (line 24 on both Leap releases), which makes FPM write to /var/log/php-fpm.log:
error_log = log/php-fpm.log
Next, open the default pool:
sudo vim /etc/php8/fpm/php-fpm.d/www.conf
The pool ships as wwwrun/www, Apache’s account. Change the user and group to nginx so the workers run as the web server account. The pool keeps listening on 127.0.0.1:9000:
user = nginx
group = nginx
When done, start and enable the service:
sudo systemctl enable --now php-fpm
Confirm it is running:
systemctl status php-fpm --no-pager
Look for active (running) and the “Ready to handle connections” status:
● php-fpm.service - The PHP FastCGI Process Manager
Loaded: loaded (/usr/lib/systemd/system/php-fpm.service; enabled; preset: disabled)
Active: active (running) since Thu 2026-10-22 15:57:01 UTC; 729ms ago
Invocation: d6f8e1016f66418fbe0b351fc865adee
Main PID: 1086 (php-fpm)
Status: "Ready to handle connections"
Tasks: 3 (limit: 1204)
CPU: 118ms
Now point Nginx at FPM. Open the main configuration file:
sudo vim /etc/nginx/nginx.conf
Find the commented block under # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 and replace it with the block below. The shipped example builds SCRIPT_FILENAME from /scripts, which is not where your files live, so the $document_root line is the change that matters:
location ~ \.php$ {
root /srv/www/htdocs/;
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
Test the syntax before reloading anything:
sudo nginx -t
Both lines must report success:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
Replace the test page so you can tell the two setups apart, restart both services and fetch it:
echo '<?php echo "PHP Hello from Nginx, PHP " . PHP_VERSION . " via " . PHP_SAPI . PHP_EOL;' | sudo tee /srv/www/htdocs/index.php
sudo systemctl restart php-fpm nginx
curl -s http://localhost/index.php
The fpm-fcgi API shows Nginx passed the request to PHP-FPM:
PHP Hello from Nginx, PHP 8.4.27 via fpm-fcgi
If the page fails instead, check that FPM is listening with sudo ss -ltnp | grep 9000, which should show php-fpm on 127.0.0.1:9000, then read /var/log/php-fpm.log and /var/log/nginx/error.log, which name the file or socket that failed.
Leap 16.0 installs with SELinux as its default security module instead of AppArmor, and we did not test these steps with SELinux enforcing. On a Leap 15.6 server with AppArmor enforcing, the php-fpm profile can also block PHP from reading files outside its allowed paths. The output below was captured when this guide was first written, and we did not re-test AppArmor in the October 2026 run. Check whether php-fpm is confined:
sudo aa-status
The php-fpm processes appeared under enforce mode:
0 profiles are in complain mode.
0 profiles are in kill mode.
0 profiles are in unconfined mode.
5 processes have profiles defined.
5 processes are in enforce mode.
/usr/sbin/avahi-daemon (773) avahi-daemon
/usr/sbin/nscd (788) nscd
/usr/sbin/php-fpm (9201) php-fpm
/usr/sbin/php-fpm (9203) php-fpm
/usr/sbin/php-fpm (9204) php-fpm
0 processes are in complain mode.
0 processes are unconfined but have a profile defined.
0 processes are in mixed mode.
0 processes are in kill mode.
If AppArmor denials show up in the journal while you debug, put the php-fpm profile into complain mode so it logs instead of blocking. Return it to enforce mode with sudo aa-enforce /etc/apparmor.d/php-fpm once the profile allows your paths:
sudo aa-complain /etc/apparmor.d/php-fpm
Then restart nginx and php-fpm:
sudo systemctl restart php-fpm nginx
Reload the test page afterwards; if it works only in complain mode, the profile needs a rule for your document root rather than staying relaxed for good.
Patch PHP2Shell (CVE-2026-109618, CVE-2026-109619)
PHP2Shell is two critical remote code execution bugs in PHP itself, CVE-2026-109618 and CVE-2026-109619, which PHP’s 20 October pre-announcement rated CVSS v4.0 9.2. They were fixed upstream on 21 October 2026 in PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35 (see the PHP security advisory). Here is where each openSUSE source stood on 22 October 2026:
- Leap 16.0 OSS repository: php8 8.4.27, fixed
- devel:languages:php:php84 on Leap 15.6: php8 8.4.27, fixed
- devel:languages:php on Leap 15.6: php8 8.5.12, fixed
- Leap 15.6 update repository: php8 8.2.30, not fixed (no CVE in its changelog)
So both install paths in this guide land on a fixed build. The stock 15.6 package is the one to worry about, and because 15.6 is past end of life you should not count on an update for it. If your application needs 8.2 or 8.3 on Leap 15.6, the same OBS project has devel:languages:php:php82 (8.2.35) and devel:languages:php:php83 (8.3.36) trees with the same URL layout as the php84 one; we checked their versions but did not install from them.
Check the installed package and its changelog. Both openSUSE packagers name the CVE IDs in the changelog, so the grep is a reliable test here:
rpm -q php8 php8-fpm
rpm -q --changelog php8 | grep CVE-2026-10961
A fixed Leap 16.0 build prints both CVE lines:
php8-8.4.27-160000.1.1.x86_64
php8-fpm-8.4.27-160000.1.1.x86_64
CVE-2026-109618: memory corruption leading to RCE
CVE-2026-109619: memory corruption leading to RCE
The php84 build on Leap 15.6 names both IDs on one line instead: Fixed GHSA-w8hw-w9fx-j79w, GHSA-6wmf-4rv5-p3rp (Avoid state corruption via bailout). (CVE-2026-109618, CVE-2026-109619). No output from the grep means your build does not carry the fix. If you just installed PHP with this guide you can stop here, because nothing older ever ran.
On an existing server, upgrade the PHP packages and include apache2-mod_php8 if Apache runs PHP through mod_php (leave it off on an FPM-only box):
sudo zypper refresh
sudo zypper update 'php8*' apache2-mod_php8
Then restart the services that run PHP, so no worker keeps the old code in memory. try-restart restarts only the units that are already running, so the same line works on an Nginx, Apache or mixed box. Long-running PHP CLI workers such as queue consumers and supervisor jobs need a restart too:
sudo systemctl try-restart php-fpm apache2
Prove the running process is newer than the package by comparing the service start time with the package install time. For mod_php use systemctl show -p ActiveEnterTimestamp apache2 and rpm -q --last apache2-mod_php8 instead:
systemctl show -p ActiveEnterTimestamp php-fpm
rpm -q --last php8-fpm
We rehearsed this on Leap 16.0 by rolling back to 8.4.26 and upgrading. The service came up 15 seconds after the package landed, which is what you want to see:
ActiveEnterTimestamp=Thu 2026-10-22 15:58:33 UTC
php8-fpm-8.4.27-160000.1.1.x86_64 Thu Oct 22 15:58:18 2026
In that test the package also restarted php-fpm on its own during the upgrade (the main PID changed from 1407 to 1603), but restart anyway and confirm, because a timestamp older than the package means the old code is still serving requests. If you added the parent devel:languages:php repository by following an older version of this guide, the upgrade will move you to PHP 8.5, as zypper list-updates showed on our 15.6 box:
v | devel:languages:php (openSUSE_Leap_15.6) | apache2-mod_php8 | 8.4.27-lp156.1.1 | 8.5.12-lp156.299.1 | x86_64
v | devel:languages:php (openSUSE_Leap_15.6) | php8 | 8.4.27-lp156.1.3 | 8.5.12-lp156.297.1 | x86_64
v | devel:languages:php (openSUSE_Leap_15.6) | php8-fpm | 8.4.27-lp156.1.1 | 8.5.12-lp156.299.1 | x86_64
PHP 8.5.12 is fixed too, so that jump is safe security-wise, but it is a new branch for your application. To stay on 8.4, remove the old repository with sudo zypper removerepo devel_languages_php and add the php84 repository from the section above before you upgrade.
Reduce exposure until you can upgrade
If you are stuck on Leap 15.6’s own PHP 8.2 for now, PHP’s advisory lists configuration changes that reduce exposure, and it is explicit that they do not make a server safe. It wants memory_limit=-1 set where applications can’t override it, and you should check that no other pool, vhost or application setting puts a finite limit back. We applied the changes on the 8.4 install from this guide, which is why the output below shows 8.4.27. For PHP-FPM, open the pool file:
sudo vim /etc/php8/fpm/php-fpm.d/www.conf
Append these two lines, then run sudo systemctl reload php-fpm:
php_admin_flag[session.upload_progress.enabled] = off
php_admin_value[memory_limit] = -1
For mod_php, set the same two values as php_admin directives in the Apache configuration, which ini_set() in application code cannot change. Create a small config file:
sudo vim /etc/apache2/conf.d/php2shell.conf
Use mod_php.c as the module name, because that is what openSUSE’s own mod_php8.conf uses. With mod_php8.c Apache accepted the file and silently ignored it in our test:
<IfModule mod_php.c>
php_admin_value memory_limit -1
php_admin_flag session.upload_progress.enabled off
</IfModule>
An ini file in /etc/php8/conf.d would also work, but that directory is shared by every SAPI (CLI, FPM and Apache) and plain ini values can still be raised by ini_set(). The advisory also asks for a low post_max_size: 8M when the application takes no uploads, 32M when it does, with upload_max_filesize kept below it. openSUSE already ships post_max_size = 8M and upload_max_filesize = 2M in the CLI, FPM and Apache php.ini files, so leave those unless your application raised them.
Restart the PHP services with sudo systemctl try-restart php-fpm apache2 so every worker picks up the change. Then check the values through the web server, not with php -r: after the Apache change the CLI on the same box still reported upload progress 1 and 128M, because it never reads the Apache or pool settings. This probe page prints what the web SAPI actually uses:
echo '<?php printf("%s %s upload_progress=%s memory_limit=%s post_max_size=%s upload_max_filesize=%s\n", PHP_VERSION, PHP_SAPI, var_export(ini_get("session.upload_progress.enabled"), true), ini_get("memory_limit"), ini_get("post_max_size"), ini_get("upload_max_filesize"));' | sudo tee /srv/www/htdocs/p2s.php
curl -s http://localhost/p2s.php
Through Apache with mod_php the page changed from upload_progress='1' memory_limit=128M to the line below, and a test script calling ini_set("memory_limit", "64M") still read back -1. Nginx with FPM showed the same '0' and -1 after the pool change:
8.4.27 apache2handler upload_progress='0' memory_limit=-1 post_max_size=8M upload_max_filesize=2M
One more measure is our own, not the advisory’s: refusing multipart uploads at Nginx with if ($http_content_type ~* "multipart/form-data") { return 403; } inside the server block. In our test a multipart POST then got 403 while urlencoded POSTs and GETs still got 200, so it breaks every file upload on the site. An unlimited memory_limit lets one request eat all RAM, so watch memory, delete p2s.php, and undo all of this once you are on a fixed build. The advisory suggests a cgroup memory cap as the availability boundary instead, which on systemd means a MemoryMax= drop-in for php-fpm (sudo systemctl edit php-fpm); we did not test that.
That gives you PHP 8.4 behind both Apache and Nginx on openSUSE, on a build that carries the PHP2Shell fix. For a database behind it, the PostgreSQL 17 on openSUSE guide is the usual next step.
Our older openSUSE guides for Node.js LTS and npm and for running openSUSE Tumbleweed in VirtualBox or VMware Workstation also apply to this box, but check their version notes against your Leap release before you follow them.