Fix bug #76922: FastCGI terminates conn after FCGI_GET_VALUES

Closes GH-12387
This commit is contained in:
Jakub Zelenka 2023-10-08 22:21:31 +01:00
parent 7e5fb564d6
commit e3d1beb0f1
No known key found for this signature in database
GPG Key ID: 1C0779DC5C0A9DE4
6 changed files with 172 additions and 8 deletions

2
NEWS
View File

@ -24,6 +24,8 @@ PHP NEWS
- FPM:
. Fixed bug GH-12232 (FPM: segfault dynamically loading extension without
opcache). (Jakub Zelenka)
. Fixed bug #76922 (FastCGI terminates conn after FCGI_GET_VALUES).
(Jakub Zelenka)
- Intl:
. Removed the BC break on IntlDateFormatter::construct which threw an

View File

@ -1202,7 +1202,7 @@ static int fcgi_read_request(fcgi_request *req)
req->keep = 0;
return 0;
}
return 0;
return 2;
} else {
return 0;
}
@ -1470,7 +1470,8 @@ int fcgi_accept_request(fcgi_request *req)
return -1;
}
req->hook.on_read();
if (fcgi_read_request(req)) {
int read_result = fcgi_read_request(req);
if (read_result == 1) {
#ifdef _WIN32
if (is_impersonate && !req->tcp) {
pipe = (HANDLE)_get_osfhandle(req->fd);
@ -1481,7 +1482,7 @@ int fcgi_accept_request(fcgi_request *req)
}
#endif
return req->fd;
} else {
} else if (read_result == 0) {
fcgi_close(req, 1, 1);
}
}

View File

@ -0,0 +1,43 @@
--TEST--
FPM: bug76922 - FCGI conn termination after FCGI_GET_VALUES
--SKIPIF--
<?php include "skipif.inc"; ?>
--FILE--
<?php
require_once "tester.inc";
$cfg = <<<EOT
[global]
error_log = {{FILE:LOG}}
[unconfined]
listen = {{ADDR}}
pm = static
pm.max_children = 1
catch_workers_output = yes
EOT;
$code = <<<EOT
<?php
echo 1;
EOT;
$tester = new FPM\Tester($cfg, $code);
$tester->start();
$tester->expectLogStartNotices();
$tester->requestValues(connKeepAlive: true)->expectValue('FCGI_MPXS_CONNS', '0');
$tester->request(connKeepAlive: true)->expectBody('1');
$tester->requestValues(connKeepAlive: true)->expectValue('FCGI_MPXS_CONNS', '0');
$tester->terminate();
$tester->close();
?>
Done
--EXPECT--
Done
--CLEAN--
<?php
require_once "tester.inc";
FPM\Tester::clean();
?>
<?php

View File

@ -60,9 +60,9 @@ class Client
const OVERLOADED = 2;
const UNKNOWN_ROLE = 3;
const MAX_CONNS = 'MAX_CONNS';
const MAX_REQS = 'MAX_REQS';
const MPXS_CONNS = 'MPXS_CONNS';
const MAX_CONNS = 'FCGI_MAX_CONNS';
const MAX_REQS = 'FCGI_MAX_REQS';
const MPXS_CONNS = 'FCGI_MPXS_CONNS';
const HEADER_LEN = 8;
@ -454,8 +454,8 @@ class Client
fwrite($this->_sock, $this->buildPacket(self::GET_VALUES, $request, 0));
$resp = $this->readPacket();
if ($resp['type'] == self::GET_VALUES_RESULT) {
return $this->readNvpair($resp['content'], $resp['length']);
if (isset($resp['type']) && $resp['type'] == self::GET_VALUES_RESULT) {
return $this->readNvpair($resp['content'], $resp['contentLength']);
} else {
throw new \Exception('Unexpected response type, expecting GET_VALUES_RESULT');
}

View File

@ -434,3 +434,89 @@ class Response
return false;
}
}
class ValuesResponse
{
/**
* @var array
*/
private array $values;
/**
* @param string|array|null $values
*/
public function __construct($values = null)
{
if ( ! is_array($values)) {
if ( ! is_null($values) ) {
$this->error('Invalid values supplied', true);
}
$this->values = [];
} else {
$this->values = $values;
}
}
/**
* Expect value.
*
* @param string $name
* @param mixed $value
* @return ValuesResponse
*/
public function expectValue(string $name, $value = null)
{
if ( ! isset($this->values[$name])) {
return $this->error("Value $name not found in values");
}
if ( ! is_null($value) && $value !== $this->values[$name]) {
return $this->error("Value $name is {$this->values[$name]} but expected $value");
}
return $this;
}
/**
* Get values.
*
* @return array
*/
public function getValues()
{
return $this->values;
}
/**
* Debug output data.
*
* @return ValuesResponse
*/
public function debugOutput()
{
echo ">>> ValuesResponse\n";
echo "----------------- Values -----------------\n";
var_dump($this->values);
echo "---------------------------------------\n\n";
return $this;
}
/**
* Emit error message
*
* @param string $message
* @param bool $throw
*
* @return ValuesResponse
*/
private function error(string $message, $throw = false): bool
{
$errorMessage = "ERROR: $message\n";
if ($throw) {
throw new \Exception($errorMessage);
}
$this->debugOutput();
echo $errorMessage;
return $this;
}
}

View File

@ -868,6 +868,38 @@ class Tester
}
}
/**
* Execute request for getting FastCGI values.
*
* @param string|null $address
* @param bool $connKeepAlive
*
* @return ValuesResponse
* @throws \Exception
*/
public function requestValues(
string $address = null,
bool $connKeepAlive = false
): ValuesResponse {
if ($this->hasError()) {
return new Response(null, true);
}
try {
$valueResponse = new ValuesResponse(
$this->getClient($address, $connKeepAlive)->getValues(['FCGI_MPXS_CONNS'])
);
if ($this->debug) {
$this->response->debugOutput();
}
} catch (\Exception $exception) {
$this->error("Request for getting values failed", $exception);
$valueResponse = new ValuesResponse();
}
return $valueResponse;
}
/**
* Get client.
*