Merge pull request #443 from v-kaywon/fix_leak

Fix leak in connection and statement resource in SQLSRV driver
This commit is contained in:
v-kaywon 2017-06-19 17:29:28 -07:00 committed by GitHub
commit a97a2f5880
6 changed files with 748 additions and 92 deletions

View file

@ -668,7 +668,11 @@ PHP_FUNCTION( sqlsrv_close )
if( zend_list_close( Z_RES_P( conn_r ) ) == FAILURE ) {
LOG( SEV_ERROR, "Failed to remove connection resource %1!d!", Z_RES_HANDLE_P( conn_r ));
}
// when conn_r is first parsed in zend_parse_parameters, conn_r becomes a zval that points to a zend_resource with a refcount of 2
// need to DELREF here so the refcount becomes 1 and conn_r can be appropriate destroyed by the garbage collector when it goes out of scope
// zend_list_close only destroy the resource pointed to by Z_RES_P( conn_r ), not the zend_resource itself
Z_TRY_DELREF_P(conn_r);
ZVAL_NULL( conn_r );
RETURN_TRUE;

View file

@ -1376,6 +1376,10 @@ PHP_FUNCTION( sqlsrv_free_stmt )
LOG( SEV_ERROR, "Failed to remove stmt resource %1!d!", Z_RES_P( stmt_r )->handle);
}
// when stmt_r is first parsed in zend_parse_parameters, stmt_r becomes a zval that points to a zend_resource with a refcount of 2
// need to DELREF here so the refcount becomes 1 and stmt_r can be appropriate destroyed by the garbage collector when it goes out of scope
// zend_list_close only destroy the resource pointed to by Z_RES_P( stmt_r ), not the zend_resource itself
Z_TRY_DELREF_P(stmt_r);
ZVAL_NULL( stmt_r );
RETURN_TRUE;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,243 @@
--TEST--
Memory Leakage Test
--DESCRIPTION--
Checks for memory leaks using memory_get_usage(). memory_get_usage() only tracks the memory that is allocated using
emalloc (which only allocate memory in the memory space allocated for the PHP process).
--ENV--
PHPT_EXEC=true
--SKIPIF--
<?php require('skipif.inc'); ?>
--FILE--
<?php
include 'MsCommon.inc';
function MemCheck($noPasses, $noRows1, $noRows2, $startStep, $endStep, $leakThreshold)
{
include 'MsSetup.inc';
$testName = "Memory Leakage Check";
StartTest($testName);
Setup();
Trace("Execution setup: $noPasses passes over a table with $noRows1 => ".($noRows1 + $noRows2)." rows.\n");
$conn1 = Connect();
CreateTable($conn1, $tableName);
$noRowsInserted = InsertRows($conn1, $tableName, $noRows1);
// Calibration
$phpLeak = RunTest($noPasses, 0, $tableName, $conn1, false, 0);
Trace("\n0. Calibration\t - PHP memory leak: $phpLeak bytes\n");
// Preliminary Execution
Trace("\nPreliminary Execution:\n");
$drvLeak = 0;
for ($j = 0; $j < 2; $j++)
{
$leak = ExecTest(1, $noRows1, $startStep, $endStep, $tableName, $conn1, (($j % 2) != 0), $phpLeak);
if ($leak > $drvLeak)
{
$drvLeak = $leak;
}
}
$totalLeak = 0;
// Execution
$noRows = $noRows1;
$prepared = false;
Trace("\nActual Execution:\n");
for ($j = 0; $j < 4; $j++)
{
switch ($j)
{
case 0:
$prepared = false;
break;
case 1:
$prepared = false;
InsertRows($conn1, $tableName, $noRows2);
$noRows += $noRows2;
break;
case 2:
$prepared = true;
break;
case 3:
$prepared = true;
InsertRows($conn1, $tableName, $noRows2);
$noRows += $noRows2;
break;
default:
break;
}
$leak = ExecTest($noPasses, $noRows, $startStep, $endStep, $tableName, $conn1, $prepared, $phpLeak) - $drvLeak;
if ($leak > $totalLeak)
{
$totalLeak = $leak;
}
}
$conn1 = null;
$conn2 = Connect();
DropTable($conn2, $tableName);
$conn2 = null;
if ($totalLeak > 0)
{
$expectedLeak = min($drvLeak, $leakThreshold) * $noPasses;
Trace("Driver memory leak: $totalLeak bytes (max expected: $expectedLeak)\n");
if ($totalLeak > $expectedLeak)
{
die("Memory leaks detected: $totalLeak bytes\n");
}
}
EndTest($testName);
}
function ExecTest($noPasses, $noRows, $startStep, $endStep, $tableName, $conn, $prepared, $phpLeak)
{
$leak = 0;
// Execution
if ($prepared)
{
Trace("\nPrepared Query Mode\n");
}
else
{
Trace("\nDirect Query Mode\n");
}
for ($i = $startStep; $i <= $endStep; $i++)
{
switch ($i)
{
case 0: // Calibration
Trace("$i. Calibration\t - ");
break;
case 1: // connection only
Trace("$i. Connection\t - ");
break;
case 2: // query
Trace("$i. Query\t - ");
break;
case 3: // fetch
Trace("$i. Fetch\t - ");
break;
default:
break;
}
$memLeak = RunTest($noPasses, $noRows, $tableName, $conn, $prepared, $i) - $phpLeak;
Trace("Driver memory leak: $memLeak bytes\n");
if ($memLeak > $leak)
{
$leak = $memLeak;
}
}
return ($leak);
}
function RunTest($noPasses, $noRows, $tableName, $conn, $prepared, $mode)
{
$leak = 0;
for ($k = 1; $k <= $noPasses; $k++)
{
$tsql = "SELECT * FROM [$tableName]";
$memStart = 0;
$memEnd = 0;
$conn2 = null;
$stmt = null;
$row = null;
$rowCount = 0;
$fldCount = 0;
$memStart = memory_get_usage();
switch ($mode)
{
case 0: // calibration
break;
case 1: // connection
$conn2 = GetConnection();
unset($conn2);
break;
case 2: // query
$stmt = ExecuteQueryEx($conn, $tsql, ($prepared ? false : true));
$fldCount = $stmt->columnCount();
$stmt->closeCursor();
unset($stmt);
break;
case 3: // fetch
$stmt = ExecuteQueryEx($conn, $tsql, ($prepared ? false : true));
$fldCount = $stmt->columnCount();
while ($row = $stmt->fetch())
{
unset($row);
$rowCount++;
}
$stmt->closeCursor();
unset($stmt);
if ($rowCount != $noRows)
{
die("$rowCount rows retrieved instead of $noRows\n");
}
break;
default:
break;
}
$memEnd = memory_get_usage();
if ($memEnd > $memStart)
{
$leak += ($memEnd - $memStart);
}
}
return ($leak);
}
function GetConnection()
{
include 'MsSetup.inc';
$conn = PDOConnect('PDO', $server, $uid, $pwd, true);
return ($conn);
}
//--------------------------------------------------------------------
// Repro
//
//--------------------------------------------------------------------
function Repro()
{
try
{
MemCheck(20, 10, 15, 1, 3, 0);
}
catch (Exception $e)
{
echo $e->getMessage();
}
}
Repro();
?>
--EXPECT--
Test "Memory Leakage Check" completed successfully.

View file

@ -0,0 +1,393 @@
--TEST--
Memory Leakage Test
--DESCRIPTION--
Checks for memory leaks using memory_get_usage(). memory_get_usage() only tracks the memory that is allocated using
emalloc (which only allocate memory in the memory space allocated for the PHP process).
--ENV--
PHPT_EXEC=true
--SKIPIF--
<?php require('skipif.inc'); ?>
--FILE--
<?php
include 'MsCommon.inc';
function MemCheck($noPasses, $noRows1, $noRows2, $startStep, $endStep)
{
include 'MsSetup.inc';
$testName = "Memory Leakage Check";
StartTest($testName);
Setup();
Trace("Execution setup: $noPasses passes over a table with $noRows1 => ".($noRows1 + $noRows2)." rows.\n");
// The data added into the table has some UTF-8 characters in it.
// The fetch functions in the switch block below fail if we don't
// set the encoding to UTF-8. We can set the UTF-8 option elsewhere
// (in the options for sqlsrv_fetch for example) but it is easier
// to simply call ConnectUTF8().
$conn1 = ConnectUTF8();
SetUTF8Data(true);
CreateTable($conn1, $tableName);
$noRowsInserted = InsertRows($conn1, $tableName, $noRows1);
// Calibration
// when fetching DateTime in the test, the DateTime PHP extension is used, and memory is allocated when this
// is extension is first used. Thus create a new DateTime and release it in the calibration step so it won't
// appear to be a leak in the testing step.
$date = new DateTime();
unset($date);
$phpLeak = RunTest($noPasses, 0, $tableName, $conn1, false, true, 0);
Trace("\n0. Calibration\t - PHP memory leak: $phpLeak bytes\n");
// Preliminary Execution
Trace("\nPreliminary Execution:\n");
$drvLeak = ExecTest(1, $noRows1, $startStep, $endStep, $tableName, $conn1, false, true, $phpLeak);
$totalLeak = 0;
// Connection & Query
$start = Max($startStep, 1);
$end = Min($endStep, 3);
Trace("\nConnection & Direct Query Execution:\n");
$leak = ExecTest($noPasses, $noRows1, $start, $end, $tableName, $conn1, false, true, $phpLeak) - $drvLeak;
if ($leak > $totalLeak)
{
$totalLeak = $leak;
}
Trace("\nPrepared Query Execution:\n");
$start = Max($startStep, 2);
$leak = ExecTest($noPasses, $noRows1, $start, $end, $tableName, $conn1, true, true, $phpLeak) - $drvLeak;
if ($leak > $totalLeak)
{
$totalLeak = $leak;
}
// Execution
$noRows = $noRows1;
$start = Max($startStep, 4);
$end = Min($endStep, 7);
$prepared = false;
$release = false;
for ($j = 0; $j < 8; $j++)
{
switch ($j)
{
case 0:
$prepared = false;
$release = true;
break;
case 1:
$prepared = true;
$release = true;
break;
case 2:
$prepared = false;
$release = false;
break;
case 3:
$prepared = true;
$release = false;
break;
case 4:
InsertRows($conn1, $tableName, $noRows2);
$noRows = $noRows1 + $noRows2;
$prepared = false;
$release = false;
break;
case 5:
$prepared = true;
$release = false;
break;
case 6:
$prepared = false;
$release = true;
break;
case 7:
$prepared = true;
$release = true;
break;
default:
break;
}
if ($prepared)
{
Trace("\nPrepared Query");
}
else
{
Trace("\nDirect Query");
}
if ($release)
{
Trace(" with statement release:\n");
}
else
{
Trace(" without statement release:\n");
}
$leak = ExecTest($noPasses, $noRows, $start, $end, $tableName, $conn1, $prepared, $release, $phpLeak) - $drvLeak;
if ($leak > $totalLeak)
{
$totalLeak = $leak;
}
}
sqlsrv_close($conn1);
$conn2 = Connect();
DropTable($conn2, $tableName);
sqlsrv_close($conn2);
SetUTF8Data(false);
if ($totalLeak > 0)
{
die("Memory leaks detected: $totalLeak bytes\n");
}
EndTest($testName);
}
function GetConnection()
{
include 'MsSetup.inc';
$conn = sqlsrv_connect($server, $connectionOptions);
return ($conn);
}
function ExecQuery($conn, $tableName, $prepared)
{
$selectQuery = "SELECT * FROM [$tableName]";
$stmt = null;
if ($prepared)
{
$stmt = sqlsrv_prepare($conn, $selectQuery);
}
else
{
$stmt = sqlsrv_query($conn, $selectQuery);
}
if ($stmt === false)
{
FatalError("Query execution failed: $selectQuery");
}
if ($prepared)
{
if (!sqlsrv_execute($stmt))
{
FatalError("Query execution failed: $selectQuery");
}
}
return ($stmt);
}
function ExecTest($noPasses, $noRows, $startStep, $endStep, $tableName, $conn, $prepared, $release, $phpLeak)
{
$leak = 0;
// Execution
for ($i = $startStep; $i <= $endStep; $i++)
{
switch ($i)
{
case 1: // connection only
Trace("$i. Connection\t - ");
break;
case 2: // query with no release
Trace("$i. Query\t - ");
break;
case 3: // query with release
Trace("$i. Query Freed\t - ");
break;
case 4: // fetch
Trace("$i. Simple Fetch\t - ");
break;
case 5: // fetch fields
Trace("$i. Fetch Fields\t - ");
break;
case 6: // fetch array
Trace("$i. Fetch Array\t - ");
break;
case 7: // fetch object
Trace("$i. Fetch Object\t - ");
break;
default:
break;
}
$memLeak = RunTest($noPasses, $noRows, $tableName, $conn, $prepared, $release, $i) - $phpLeak;
Trace("Driver memory leak: $memLeak bytes\n");
if ($memLeak > 0)
{
if ($leak <= 0)
{
$leak = $memLeak;
echo intval($leak) . " leaking\n";
}
}
}
return ($leak);
}
function RunTest($noPasses, $noRows, $tableName, $conn, $prepared, $release, $mode)
{
$memStart = memory_get_usage();
for ($k = 1; $k <= $noPasses; $k++)
{
$conn2 = null;
$stmt = null;
$fld = null;
$rowCount = 0;
$numFields = 0;
$i = 0;
switch ($mode)
{
case 0: // calibration
break;
case 1: // no release
$conn2 = GetConnection();
sqlsrv_close($conn2);
break;
case 2: // query with no release
$stmt = ExecQuery($conn, $tableName, $prepared);
break;
case 3: // query with release
$stmt = ExecQuery($conn, $tableName, $prepared);
sqlsrv_free_stmt($stmt);
break;
case 4: // fetch
$stmt = ExecQuery($conn, $tableName, $prepared);
while (sqlsrv_fetch($stmt))
{
$rowCount++;
}
if ($release)
{
sqlsrv_free_stmt($stmt);
}
if ($rowCount != $noRows)
{
die("$rowCount rows retrieved instead of $noRows\n");
}
break;
case 5: // fetch fields
$stmt = ExecQuery($conn, $tableName, $prepared);
$numFields = sqlsrv_num_fields($stmt);
while (sqlsrv_fetch($stmt))
{
$rowCount++;
for ($i = 0; $i < $numFields; $i++)
{
$fld = sqlsrv_get_field($stmt, $i);
if ($fld === false)
{
die("Field $i of row $rowCount is missing");
}
unset($fld);
}
}
if ($release)
{
sqlsrv_free_stmt($stmt);
}
if ($rowCount != $noRows)
{
die("$rowCount rows retrieved instead of $noRows\n");
}
break;
case 6: // fetch array
$stmt = ExecQuery($conn, $tableName, $prepared);
While (sqlsrv_fetch_array($stmt))
{
$rowCount++;
}
if ($release)
{
sqlsrv_free_stmt($stmt);
}
if ($rowCount != $noRows)
{
die("$rowCount rows retrieved instead of $noRows\n");
}
break;
case 7: // fetch object
$stmt = ExecQuery($conn, $tableName, $prepared);
While (sqlsrv_fetch_object($stmt))
{
$rowCount++;
}
if ($release)
{
sqlsrv_free_stmt($stmt);
}
if ($rowCount != $noRows)
{
die("$rowCount rows retrieved instead of $noRows\n");
}
break;
default:
break;
}
// need unset to trigger the destruction of a zval with refcount of 0
unset($conn2);
unset($stmt);
}
$memEnd = memory_get_usage();
Trace( intval($memEnd) . " - " . intval($memStart) . "\n" );
return ($memEnd - $memStart);
}
//--------------------------------------------------------------------
// Repro
//
//--------------------------------------------------------------------
function Repro()
{
try
{
MemCheck(20, 10, 15, 1, 7);
}
catch (Exception $e)
{
echo $e->getMessage();
}
}
Repro();
?>
--EXPECT--
Test "Memory Leakage Check" completed successfully.