Warn when fpm socket was not registered on the expected path

This might happen if the UDS length limit is exceeded.

Co-authored-by: Jakub Zelenka <bukka@php.net>

Closes GH-11066
This commit is contained in:
Joshua Behrens 2023-04-12 23:17:03 +02:00 committed by Jakub Zelenka
parent 72e2e25066
commit 08b57772b0
No known key found for this signature in database
GPG key ID: 1C0779DC5C0A9DE4
8 changed files with 217 additions and 10 deletions

4
NEWS
View file

@ -22,6 +22,10 @@ PHP NEWS
. Added DOMElement::className and DOMElement::id. (nielsdos)
. Added DOMParentNode::replaceChildren(). (nielsdos)
- FPM:
. Added warning to log when fpm socket was not registered on the expected
path. (Joshua Behrens, Jakub Zelenka)
- Intl:
. Fix memory leak in MessageFormatter::format() on failure. (Girgias)

View file

@ -429,6 +429,9 @@ PHP 8.3 UPGRADE NOTES
current system user. Previously, calling FFI::load() was not possible during
preloading if the opcache.preload_user directive was set.
- FPM:
. FPM CLI test now fails if the socket path is longer than supported by OS.
- Opcache:
. In the cli and phpdbg SAPIs, preloading does not require the
opcache.preload_user directive to be set anymore when running as root. In

View file

@ -396,9 +396,25 @@ static int fpm_socket_af_inet_listening_socket(struct fpm_worker_pool_s *wp) /*
static int fpm_socket_af_unix_listening_socket(struct fpm_worker_pool_s *wp) /* {{{ */
{
struct sockaddr_un sa_un;
size_t socket_length = sizeof(sa_un.sun_path);
size_t address_length = strlen(wp->config->listen_address);
memset(&sa_un, 0, sizeof(sa_un));
strlcpy(sa_un.sun_path, wp->config->listen_address, sizeof(sa_un.sun_path));
strlcpy(sa_un.sun_path, wp->config->listen_address, socket_length);
if (address_length >= socket_length) {
zlog(
ZLOG_WARNING,
"[pool %s] cannot bind to UNIX socket '%s' as path is too long (found length: %zu, "
"maximal length: %zu), trying cut socket path instead '%s'",
wp->config->name,
wp->config->listen_address,
address_length,
socket_length,
sa_un.sun_path
);
}
sa_un.sun_family = AF_UNIX;
return fpm_sockets_get_listening_socket(wp, (struct sockaddr *) &sa_un, sizeof(struct sockaddr_un));
}

View file

@ -8,6 +8,7 @@
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/un.h>
#include <pwd.h>
#include <grp.h>
@ -63,6 +64,33 @@ static struct passwd *fpm_unix_get_passwd(struct fpm_worker_pool_s *wp, const ch
return pwd;
}
static inline bool fpm_unix_check_listen_address(struct fpm_worker_pool_s *wp, const char *address, int flags)
{
if (wp->listen_address_domain != FPM_AF_UNIX) {
return true;
}
struct sockaddr_un test_socket;
size_t address_length = strlen(address);
size_t socket_length = sizeof(test_socket.sun_path);
if (address_length < socket_length) {
return true;
}
zlog(
flags,
"[pool %s] cannot bind to UNIX socket '%s' as path is too long (found length: %zu, "
"maximal length: %zu)",
wp->config->name,
address,
address_length,
socket_length
);
return false;
}
static inline bool fpm_unix_check_passwd(struct fpm_worker_pool_s *wp, const char *name, int flags)
{
return !name || fpm_unix_is_id(name) || fpm_unix_get_passwd(wp, name, flags);
@ -90,6 +118,7 @@ bool fpm_unix_test_config(struct fpm_worker_pool_s *wp)
return (
fpm_unix_check_passwd(wp, config->user, ZLOG_ERROR) &&
fpm_unix_check_group(wp, config->group, ZLOG_ERROR) &&
fpm_unix_check_listen_address(wp, config->listen_address, ZLOG_SYSERROR) &&
fpm_unix_check_passwd(wp, config->listen_owner, ZLOG_SYSERROR) &&
fpm_unix_check_group(wp, config->listen_group, ZLOG_SYSERROR)
);
@ -273,7 +302,7 @@ int fpm_unix_set_socket_permissions(struct fpm_worker_pool_s *wp, const char *pa
/* Copy the new ACL entry from config */
for (i=ACL_FIRST_ENTRY ; acl_get_entry(aclconf, i, &entryconf) ; i=ACL_NEXT_ENTRY) {
if (0 > acl_create_entry (&aclfile, &entryfile) ||
0 > acl_copy_entry(entryfile, entryconf)) {
0 > acl_copy_entry(entryfile, entryconf)) {
zlog(ZLOG_SYSERROR, "[pool %s] failed to add entry to the ACL of the socket '%s'", wp->config->name, path);
acl_free(aclfile);
return -1;

View file

@ -16,7 +16,7 @@ class LogReader
*
* @var string|null
*/
private ?string $currentSourceName;
private ?string $currentSourceName = null;
/**
* Log descriptors.

View file

@ -0,0 +1,74 @@
--TEST--
FPM: UNIX socket filename is too for start
--SKIPIF--
<?php
include "skipif.inc"; ?>
--FILE--
<?php
require_once "tester.inc";
$socketFilePrefix = __DIR__ . '/socket-file';
$socketFile = sprintf(
"%s-fpm-unix-socket-too-long-filename-but-starts-anyway%s.sock",
$socketFilePrefix,
str_repeat('-0000', 11)
);
$cfg = <<<EOT
[global]
error_log = {{FILE:LOG}}
[fpm_pool]
listen = $socketFile
pm = static
pm.max_children = 1
catch_workers_output = yes
EOT;
$tester = new FPM\Tester($cfg);
$tester->start();
$tester->expectLogStartNotices();
$tester->expectLogPattern(
sprintf(
'/\[pool fpm_pool\] cannot bind to UNIX socket \'%s\' as path is too long '
. '\(found length: %d, maximal length: \d+\), trying cut socket path instead \'.+\'/',
preg_quote($socketFile, '/'),
strlen($socketFile)
),
true
);
$files = glob($socketFilePrefix . '*');
if ($files === []) {
echo 'Socket files were not found.' . PHP_EOL;
}
if ($socketFile === $files[0]) {
// this means the socket file path length is not an issue (anymore). Might be not long enough
echo 'Socket file is the same as configured.' . PHP_EOL;
}
$tester->terminate();
$tester->expectLogTerminatingNotices();
$tester->close();
?>
Done
--EXPECT--
Done
--CLEAN--
<?php
require_once "tester.inc";
FPM\Tester::clean();
// cleanup socket file if php-fpm was not killed
$socketFile = sprintf(
"/socket-file-fpm-unix-socket-too-long-filename-but-starts-anyway%s.sock",
__DIR__,
str_repeat('-0000', 11)
);
if (is_file($socketFile)) {
unlink($socketFile);
}
?>

View file

@ -0,0 +1,47 @@
--TEST--
FPM: UNIX socket filename is too for test
--SKIPIF--
<?php
include "skipif.inc"; ?>
--FILE--
<?php
require_once "tester.inc";
$socketFilePrefix = __DIR__ . '/socket-file';
$socketFile = sprintf(
"/socket-file-fpm-unix-socket-too-long-filename-but-starts-anyway%s.sock",
__DIR__,
str_repeat('-0000', 11)
);
$cfg = <<<EOT
[global]
error_log = {{FILE:LOG}}
[fpm_pool]
listen = $socketFile
pm = static
pm.max_children = 1
catch_workers_output = yes
EOT;
$tester = new FPM\Tester($cfg);
$tester->testConfig(true, [
sprintf(
'/cannot bind to UNIX socket \'%s\' as path is too long '
. '\(found length: %d, maximal length: \d+\)/',
preg_quote($socketFile, '/'),
strlen($socketFile)
),
'/FPM initialization failed/',
]);
?>
Done
--EXPECT--
Done
--CLEAN--
<?php
require_once "tester.inc";
FPM\Tester::clean();
?>

View file

@ -382,26 +382,50 @@ class Tester
* @return null|array
* @throws \Exception
*/
public function testConfig($silent = false)
public function testConfig($silent = false, array|string|null $expectedPattern = null): ?array
{
$configFile = $this->createConfig();
$cmd = self::findExecutable() . ' -n -tt -y ' . $configFile . ' 2>&1';
$this->trace('Testing config using command', $cmd, true);
exec($cmd, $output, $code);
$found = 0;
if ($expectedPattern !== null) {
$expectedPatterns = is_array($expectedPattern) ? $expectedPattern : [$expectedPattern];
}
if ($code) {
$messages = [];
foreach ($output as $outputLine) {
$message = preg_replace("/\[.+?\]/", "", $outputLine, 1);
if ($expectedPattern !== null) {
for ($i = 0; $i < count($expectedPatterns); $i++) {
$pattern = $expectedPatterns[$i];
if ($pattern !== null && preg_match($pattern, $message)) {
$found++;
$expectedPatterns[$i] = null;
}
}
}
$messages[] = $message;
if ( ! $silent) {
$this->error($message, null, false);
}
}
return $messages;
} else {
$messages = null;
}
return null;
if ($expectedPattern !== null && $found < count($expectedPatterns)) {
$missingPatterns = array_filter($expectedPatterns);
$errorMessage = sprintf(
"The expected config %s %s %s not been found",
count($missingPatterns) > 1 ? 'patterns' : 'pattern',
implode(', ', $missingPatterns),
count($missingPatterns) > 1 ? 'have' : 'has',
);
$this->error($errorMessage);
}
return $messages;
}
/**
@ -1155,9 +1179,19 @@ class Tester
return $address;
}
return sys_get_temp_dir() . '/' .
hash('crc32', dirname($address)) . '-' .
basename($address);
$addressPart = hash('crc32', dirname($address)) . '-' . basename($address);
// is longer on Mac, than on Linux
$tmpDirAddress = sys_get_temp_dir() . '/' . $addressPart;
;
if (strlen($tmpDirAddress) <= 104) {
return $tmpDirAddress;
}
$srcRootAddress = dirname(__DIR__, 3) . '/' . $addressPart;
return $srcRootAddress;
}
return $this->getHost($type) . ':' . $port;