Skip to content

Commit f855b9b

Browse files
committed
GH-23423: Return correct return value when write fails
php_handle_aborted_connection did not always abort, i.e. if ignore_user_abort=1. So if sending fails and ret is 0, then remaining is never decremented and this loops forever. Fix #23423
1 parent 5ccc56b commit f855b9b

5 files changed

Lines changed: 110 additions & 1 deletion

File tree

‎.github/workflows/unit-tests.yml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ on:
55
- 'main/network.c'
66
- 'main/io/**'
77
- 'main/php_io.h'
8+
- 'sapi/embed/**'
89
- 'tests/unit/**'
910
- '.github/workflows/unit-tests.yml'
1011
branches:
@@ -14,6 +15,7 @@ on:
1415
- 'main/network.c'
1516
- 'main/io/**'
1617
- 'main/php_io.h'
18+
- 'sapi/embed/**'
1719
- 'tests/unit/**'
1820
- '.github/workflows/unit-tests.yml'
1921
branches:

‎NEWS‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ PHP NEWS
99
passing an array of CURLFile or CURLStringFile objects to
1010
CURLOPT_POSTFIELDS. (Sjoerd Langkemper)
1111

12+
- Embed:
13+
. Fixed infinite loop when a write fails and the connection is not aborted
14+
(GH-23423). (Sjoerd Langkemper)
15+
1216
- FPM:
1317
. Use recommended TYPE, HELP order for OpenMetrics metadata.
1418
(Marcel Hernandez)

‎sapi/embed/php_embed.c‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ static size_t php_embed_ub_write(const char *str, size_t str_length)
7575
ret = php_embed_single_write(ptr, remaining);
7676
if (!ret) {
7777
php_handle_aborted_connection();
78+
return str_length - remaining;
7879
}
7980
ptr += ret;
8081
remaining -= ret;

‎tests/unit/Makefile‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ CFLAGS = -g -Wall -I../../ -I../../Zend -I../../main -I../../TSRM -I. -I..
33
COMMON_LDFLAGS = ../../.libs/libphp.a -lcmocka -lpthread -lm -ldl -lresolv -lutil
44

55
# Update paths in .github/workflows/unit-tests.yml when adding new test to make it run in PR when such file changes
6-
TESTS = main/test_network main/test_io_copy
6+
TESTS = main/test_network main/test_io_copy sapi/embed/test_embed_ub_write
77
main/test_network_SRC = main/test_network.c
88
main/test_network_LDFLAGS = $(COMMON_LDFLAGS) -Wl,--wrap=connect,--wrap=poll,--wrap=getsockopt,--wrap=gettimeofday
99
main/test_io_copy_SRC = main/test_io_copy.c
1010
main/test_io_copy_LDFLAGS = $(COMMON_LDFLAGS) -Wl,--wrap=copy_file_range,--wrap=sendfile,--wrap=splice,--wrap=read,--wrap=write,--wrap=poll,--wrap=setsockopt
11+
sapi/embed/test_embed_ub_write_SRC = sapi/embed/test_embed_ub_write.c
12+
sapi/embed/test_embed_ub_write_LDFLAGS = $(COMMON_LDFLAGS) -Wl,--wrap=write
1113

1214

1315
# Build all tests
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
#include "php.h"
2+
#include "sapi/embed/php_embed.h"
3+
#include <unistd.h>
4+
#include <cmocka.h>
5+
6+
#ifdef PHP_WRITE_STDOUT
7+
ssize_t __wrap_write(int fd, const void *buf, size_t count)
8+
{
9+
function_called();
10+
check_expected(fd);
11+
check_expected(count);
12+
return mock_type(ssize_t);
13+
}
14+
15+
struct ub_write_state {
16+
bool ignore_user_abort;
17+
short connection_status;
18+
};
19+
20+
static int setup_ub_write(void **state)
21+
{
22+
struct ub_write_state *saved = malloc(sizeof(*saved));
23+
24+
assert_non_null(saved);
25+
saved->ignore_user_abort = PG(ignore_user_abort);
26+
saved->connection_status = PG(connection_status);
27+
28+
/* Keep php_handle_aborted_connection() from bailing out on failure. */
29+
PG(ignore_user_abort) = true;
30+
PG(connection_status) = PHP_CONNECTION_NORMAL;
31+
32+
*state = saved;
33+
return 0;
34+
}
35+
36+
static int teardown_ub_write(void **state)
37+
{
38+
struct ub_write_state *saved = *state;
39+
40+
PG(ignore_user_abort) = saved->ignore_user_abort;
41+
PG(connection_status) = saved->connection_status;
42+
free(saved);
43+
return 0;
44+
}
45+
46+
/* A failed write without progress must not retry forever. */
47+
static void test_ub_write_error_without_progress(void **state)
48+
{
49+
expect_function_call(__wrap_write);
50+
expect_value(__wrap_write, fd, STDOUT_FILENO);
51+
expect_value(__wrap_write, count, 16);
52+
will_return(__wrap_write, 0);
53+
54+
assert_int_equal(php_embed_module.ub_write("0123456789abcdef", 16), 0);
55+
assert_int_equal(PG(connection_status), PHP_CONNECTION_ABORTED);
56+
}
57+
58+
/* After a partial write, a failure must return the number of bytes written. */
59+
static void test_ub_write_partial_then_error(void **state)
60+
{
61+
expect_function_calls(__wrap_write, 2);
62+
expect_value_count(__wrap_write, fd, STDOUT_FILENO, 2);
63+
expect_value(__wrap_write, count, 10);
64+
expect_value(__wrap_write, count, 6);
65+
will_return(__wrap_write, 4);
66+
will_return(__wrap_write, 0);
67+
68+
assert_int_equal(php_embed_module.ub_write("0123456789", 10), 4);
69+
assert_int_equal(PG(connection_status), PHP_CONNECTION_ABORTED);
70+
}
71+
72+
/* Partial writes must continue until the whole buffer is written. */
73+
static void test_ub_write_partial_writes(void **state)
74+
{
75+
expect_function_calls(__wrap_write, 2);
76+
expect_value_count(__wrap_write, fd, STDOUT_FILENO, 2);
77+
expect_value(__wrap_write, count, 10);
78+
expect_value(__wrap_write, count, 6);
79+
will_return(__wrap_write, 4);
80+
will_return(__wrap_write, 6);
81+
82+
assert_int_equal(php_embed_module.ub_write("0123456789", 10), 10);
83+
assert_int_equal(PG(connection_status), PHP_CONNECTION_NORMAL);
84+
}
85+
#endif
86+
87+
int main(void)
88+
{
89+
#ifdef PHP_WRITE_STDOUT
90+
const struct CMUnitTest tests[] = {
91+
cmocka_unit_test_setup_teardown(test_ub_write_error_without_progress, setup_ub_write, teardown_ub_write),
92+
cmocka_unit_test_setup_teardown(test_ub_write_partial_then_error, setup_ub_write, teardown_ub_write),
93+
cmocka_unit_test_setup_teardown(test_ub_write_partial_writes, setup_ub_write, teardown_ub_write),
94+
};
95+
96+
return cmocka_run_group_tests(tests, NULL, NULL);
97+
#else
98+
return 0;
99+
#endif
100+
}

0 commit comments

Comments
 (0)