Skip to content

Commit fa0df82

Browse files
committed
Fix pdo_odbc output-buffer leak and stale-binding out-of-bounds read
SQLBindParameter recorded deferred pointers (P->outbuf, &P->len, and the bound-parameter struct) into the ODBC statement at PDO_PARAM_EVT_ALLOC, and P->outbuf was never freed. Both outlive the bound-parameter struct, which PDO destroys mid-statement on execute() with an array or a rebind: the buffer leaked and the stale binding made the next SQLExecute read freed memory. Track output buffers on the statement and free them in the destructor, and defer binding to execute time, resetting and rebinding the current parameters when the set changed.
1 parent 345b88d commit fa0df82

4 files changed

Lines changed: 129 additions & 15 deletions

File tree

ext/pdo_odbc/odbc_stmt.c

Lines changed: 65 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,11 @@ static void free_cols(pdo_stmt_t *stmt, pdo_odbc_stmt *S)
133133
}
134134
}
135135

136+
static void odbc_free_out_buffer(zval *el)
137+
{
138+
efree(Z_PTR_P(el));
139+
}
140+
136141
static int odbc_stmt_dtor(pdo_stmt_t *stmt)
137142
{
138143
pdo_odbc_stmt *S = (pdo_odbc_stmt*)stmt->driver_data;
@@ -153,11 +158,41 @@ static int odbc_stmt_dtor(pdo_stmt_t *stmt)
153158
if (S->convbuf) {
154159
efree(S->convbuf);
155160
}
161+
if (S->out_buffers) {
162+
zend_hash_destroy(S->out_buffers);
163+
FREE_HASHTABLE(S->out_buffers);
164+
}
156165
efree(S);
157166

158167
return 1;
159168
}
160169

170+
static int odbc_bind_param(pdo_stmt_t *stmt, struct pdo_bound_param_data *param)
171+
{
172+
pdo_odbc_stmt *S = (pdo_odbc_stmt*)stmt->driver_data;
173+
pdo_odbc_param *P = (pdo_odbc_param*)param->driver_data;
174+
RETCODE rc;
175+
176+
if (!P) {
177+
return 1;
178+
}
179+
180+
rc = SQLBindParameter(S->stmt, (SQLUSMALLINT) param->paramno+1,
181+
P->paramtype, P->ctype, P->sqltype, P->precision, P->scale,
182+
P->paramtype == SQL_PARAM_INPUT ?
183+
(SQLPOINTER)param :
184+
P->outbuf,
185+
P->outbuf ? P->outbuflen : 0,
186+
&P->len
187+
);
188+
189+
if (rc == SQL_SUCCESS || rc == SQL_SUCCESS_WITH_INFO) {
190+
return 1;
191+
}
192+
pdo_odbc_stmt_error("SQLBindParameter");
193+
return 0;
194+
}
195+
161196
static int odbc_stmt_execute(pdo_stmt_t *stmt)
162197
{
163198
RETCODE rc, rc1;
@@ -169,6 +204,24 @@ static int odbc_stmt_execute(pdo_stmt_t *stmt)
169204
SQLCloseCursor(S->stmt);
170205
}
171206

207+
if (S->params_dirty) {
208+
rc = SQLFreeStmt(S->stmt, SQL_RESET_PARAMS);
209+
if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) {
210+
pdo_odbc_stmt_error("SQLFreeStmt: SQL_RESET_PARAMS");
211+
return 0;
212+
}
213+
if (stmt->bound_params) {
214+
struct pdo_bound_param_data *param;
215+
216+
ZEND_HASH_FOREACH_PTR(stmt->bound_params, param) {
217+
if (!odbc_bind_param(stmt, param)) {
218+
return 0;
219+
}
220+
} ZEND_HASH_FOREACH_END();
221+
}
222+
S->params_dirty = 0;
223+
}
224+
172225
rc = SQLExecute(S->stmt);
173226

174227
while (rc == SQL_NEED_DATA) {
@@ -312,6 +365,7 @@ static int odbc_stmt_param_hook(pdo_stmt_t *stmt, struct pdo_bound_param_data *p
312365
if (P) {
313366
efree(P);
314367
}
368+
S->params_dirty = 1;
315369
break;
316370

317371
case PDO_PARAM_EVT_ALLOC:
@@ -386,6 +440,11 @@ static int odbc_stmt_param_hook(pdo_stmt_t *stmt, struct pdo_bound_param_data *p
386440
}
387441
P->outbuf = emalloc(P->len + (P->is_unicode ? 2:1));
388442
P->outbuflen = P->len;
443+
if (!S->out_buffers) {
444+
ALLOC_HASHTABLE(S->out_buffers);
445+
zend_hash_init(S->out_buffers, 8, NULL, odbc_free_out_buffer, 0);
446+
}
447+
zend_hash_next_index_insert_ptr(S->out_buffers, P->outbuf);
389448
}
390449
}
391450

@@ -394,20 +453,12 @@ static int odbc_stmt_param_hook(pdo_stmt_t *stmt, struct pdo_bound_param_data *p
394453
return 0;
395454
}
396455

397-
rc = SQLBindParameter(S->stmt, (SQLUSMALLINT) param->paramno+1,
398-
P->paramtype, ctype, sqltype, precision, scale,
399-
P->paramtype == SQL_PARAM_INPUT ?
400-
(SQLPOINTER)param :
401-
P->outbuf,
402-
P->len,
403-
&P->len
404-
);
405-
406-
if (rc == SQL_SUCCESS || rc == SQL_SUCCESS_WITH_INFO) {
407-
return 1;
408-
}
409-
pdo_odbc_stmt_error("SQLBindParameter");
410-
return 0;
456+
P->sqltype = sqltype;
457+
P->ctype = ctype;
458+
P->precision = precision;
459+
P->scale = scale;
460+
S->params_dirty = 1;
461+
return 1;
411462
}
412463

413464
case PDO_PARAM_EVT_EXEC_PRE:

ext/pdo_odbc/php_pdo_odbc_int.h

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,16 +145,22 @@ typedef struct {
145145
pdo_odbc_errinfo einfo;
146146
char *convbuf;
147147
zend_ulong convbufsize;
148+
HashTable *out_buffers;
148149
unsigned going_long:1;
149150
unsigned assume_utf8:1;
151+
unsigned params_dirty:1;
150152
signed col_count:16;
151-
unsigned _spare:14;
153+
unsigned _spare:13;
152154
} pdo_odbc_stmt;
153155

154156
typedef struct {
155157
SQLLEN len;
156158
SQLLEN outbuflen;
157159
SQLSMALLINT paramtype;
160+
SQLSMALLINT sqltype;
161+
SQLSMALLINT ctype;
162+
SQLSMALLINT scale;
163+
SQLULEN precision;
158164
char *outbuf;
159165
unsigned is_unicode:1;
160166
unsigned _spare:31;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
--TEST--
2+
pdo_odbc: a bound parameter does not leave a stale ODBC binding on re-execute
3+
--EXTENSIONS--
4+
pdo_odbc
5+
--SKIPIF--
6+
<?php
7+
require __DIR__ . '/config.inc';
8+
try {
9+
$pdo = new PDO(PDO_ODBC_SQLITE_DSN);
10+
} catch (PDOException $e) {
11+
die("skip requires the SQLite3 ODBC driver");
12+
}
13+
?>
14+
--FILE--
15+
<?php
16+
require __DIR__ . '/config.inc';
17+
$pdo = new PDO(PDO_ODBC_SQLITE_DSN);
18+
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
19+
20+
$stmt = $pdo->prepare('SELECT ? AS v');
21+
$v = 'bound';
22+
$stmt->bindParam(1, $v, PDO::PARAM_STR | PDO::PARAM_INPUT_OUTPUT, 256);
23+
$stmt->execute(['from_array']);
24+
25+
var_dump($stmt->fetch(PDO::FETCH_ASSOC)['v']);
26+
?>
27+
--EXPECT--
28+
string(10) "from_array"
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
--TEST--
2+
pdo_odbc: bound output parameter buffer is released (no leak)
3+
--EXTENSIONS--
4+
pdo_odbc
5+
--SKIPIF--
6+
<?php
7+
require __DIR__ . '/config.inc';
8+
try {
9+
$pdo = new PDO(PDO_ODBC_SQLITE_DSN);
10+
} catch (PDOException $e) {
11+
die("skip requires the SQLite3 ODBC driver");
12+
}
13+
?>
14+
--FILE--
15+
<?php
16+
require __DIR__ . '/config.inc';
17+
$pdo = new PDO(PDO_ODBC_SQLITE_DSN);
18+
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
19+
20+
$stmt = $pdo->prepare('SELECT ? AS v');
21+
$var = 'seed';
22+
$stmt->bindParam(1, $var, PDO::PARAM_STR | PDO::PARAM_INPUT_OUTPUT, 256);
23+
$stmt->execute();
24+
$row = $stmt->fetch(PDO::FETCH_ASSOC);
25+
26+
var_dump($row['v']);
27+
?>
28+
--EXPECT--
29+
string(4) "seed"

0 commit comments

Comments
 (0)