Skip to content

Commit a698573

Browse files
ext/pdo_pgsql: Fix the connection state left behind by a lazy fetch
1 parent 0db3910 commit a698573

5 files changed

Lines changed: 117 additions & 6 deletions

File tree

NEWS

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,14 @@ PHP NEWS
4141
. Fixed bug GH-23016 (NULL values in long columns come back as garbage
4242
binary strings). (Calvin Buckley, iliaal)
4343

44+
- PDO_PGSQL:
45+
. Fixed a lazy fetch (PDO::ATTR_PREFETCH => 0) leaving the connection busy
46+
for the next one when no prepared statement is used. (KentarouTakeda)
47+
. Fixed a use-after-free after destroying a statement when no prepared
48+
statement is used. (KentarouTakeda)
49+
. Fixed a lazy fetch returning a row of NULLs after another statement took
50+
over the connection. (KentarouTakeda)
51+
4452
- Reflection:
4553
. Fixed bug GH-22905 (Reflection exception messages truncate on null bytes).
4654
(DanielEScherzer)

ext/pdo_pgsql/pgsql_statement.c

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ static void pgsql_stmt_finish(pdo_pgsql_stmt *S, int fin_mode)
7171
char errbuf[256];
7272
PQcancel(cancel, errbuf, 256);
7373
PQfreeCancel(cancel);
74-
S->is_running_unbuffered = false;
7574
}
7675

7776
if (S->result) {
@@ -113,9 +112,6 @@ static void pgsql_stmt_finish(pdo_pgsql_stmt *S, int fin_mode)
113112
}
114113

115114
S->is_prepared = false;
116-
if (H->running_stmt == S) {
117-
H->running_stmt = NULL;
118-
}
119115
}
120116
}
121117

@@ -126,6 +122,10 @@ static int pgsql_stmt_dtor(pdo_stmt_t *stmt)
126122

127123
pgsql_stmt_finish(S, FIN_DISCARD|(server_obj_usable ? FIN_CLOSE|FIN_ABORT : 0));
128124

125+
if (server_obj_usable && S->H->running_stmt == S) {
126+
S->H->running_stmt = NULL;
127+
}
128+
129129
if (S->stmt_name) {
130130
efree(S->stmt_name);
131131
S->stmt_name = NULL;
@@ -590,12 +590,12 @@ static int pgsql_stmt_fetch(pdo_stmt_t *stmt,
590590
S->current_row = 0;
591591

592592
if (!stmt->row_count) {
593-
S->is_running_unbuffered = false;
594593
/* libpq requires looping until getResult returns null */
595594
pgsql_stmt_finish(S, 0);
596595
}
597596
}
598-
if (S->current_row < stmt->row_count) {
597+
/* another statement may have taken over and freed the result */
598+
if (S->result && S->current_row < stmt->row_count) {
599599
S->current_row++;
600600
return 1;
601601
} else {
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
--TEST--
2+
PDO PgSQL an abandoned lazy fetch frees the connection without a prepared statement
3+
--EXTENSIONS--
4+
pdo
5+
pdo_pgsql
6+
--SKIPIF--
7+
<?php
8+
require __DIR__ . '/config.inc';
9+
require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc';
10+
PDOTest::skip();
11+
?>
12+
--FILE--
13+
<?php
14+
15+
require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc';
16+
$pdo = PDOTest::test_factory(__DIR__ . '/common.phpt');
17+
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
18+
19+
foreach ([
20+
'PDO::ATTR_EMULATE_PREPARES' => [PDO::ATTR_EMULATE_PREPARES => true],
21+
'Pdo\Pgsql::ATTR_DISABLE_PREPARES' => [Pdo\Pgsql::ATTR_DISABLE_PREPARES => true],
22+
] as $label => $options) {
23+
$options[PDO::ATTR_PREFETCH] = 0;
24+
25+
$stmt = $pdo->prepare("VALUES (1), (2)", $options);
26+
$stmt->execute();
27+
$stmt = null;
28+
29+
$stmt = $pdo->prepare("VALUES (1), (2)", $options);
30+
$stmt->execute();
31+
echo "$label: ";
32+
var_dump((bool) $stmt->fetchAll());
33+
}
34+
?>
35+
--EXPECT--
36+
PDO::ATTR_EMULATE_PREPARES: bool(true)
37+
Pdo\Pgsql::ATTR_DISABLE_PREPARES: bool(true)
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
--TEST--
2+
PDO PgSQL a drained lazy fetch frees the connection without a prepared statement
3+
--EXTENSIONS--
4+
pdo
5+
pdo_pgsql
6+
--SKIPIF--
7+
<?php
8+
require __DIR__ . '/config.inc';
9+
require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc';
10+
PDOTest::skip();
11+
?>
12+
--FILE--
13+
<?php
14+
15+
require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc';
16+
$pdo = PDOTest::test_factory(__DIR__ . '/common.phpt');
17+
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
18+
19+
foreach ([
20+
'PDO::ATTR_EMULATE_PREPARES' => [PDO::ATTR_EMULATE_PREPARES => true],
21+
'Pdo\Pgsql::ATTR_DISABLE_PREPARES' => [Pdo\Pgsql::ATTR_DISABLE_PREPARES => true],
22+
] as $label => $options) {
23+
$options[PDO::ATTR_PREFETCH] = 0;
24+
25+
$stmt = $pdo->prepare("VALUES (1), (2)", $options);
26+
$stmt->execute();
27+
$stmt->fetchAll();
28+
29+
$stmt = $pdo->prepare("VALUES (1), (2)", $options);
30+
$stmt->execute();
31+
echo "$label: ";
32+
var_dump((bool) $stmt->fetchAll());
33+
}
34+
?>
35+
--EXPECT--
36+
PDO::ATTR_EMULATE_PREPARES: bool(true)
37+
Pdo\Pgsql::ATTR_DISABLE_PREPARES: bool(true)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
--TEST--
2+
PDO PgSQL a lazy fetch whose stream was taken over reports no leftover rows
3+
--EXTENSIONS--
4+
pdo
5+
pdo_pgsql
6+
--SKIPIF--
7+
<?php
8+
require __DIR__ . '/config.inc';
9+
require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc';
10+
PDOTest::skip();
11+
?>
12+
--FILE--
13+
<?php
14+
15+
require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc';
16+
$pdo = PDOTest::test_factory(__DIR__ . '/common.phpt');
17+
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
18+
$pdo->setAttribute(PDO::ATTR_PREFETCH, 0);
19+
20+
$first = $pdo->prepare("VALUES (1), (2)");
21+
$first->execute();
22+
23+
$pdo->prepare("VALUES (1), (2)")->execute();
24+
25+
var_dump($first->fetchAll(PDO::FETCH_NUM));
26+
?>
27+
--EXPECT--
28+
array(0) {
29+
}

0 commit comments

Comments
 (0)