Skip to content

Commit 5d16756

Browse files
authored
fix(agent): fix potential memleak when calling newrelic_end_transaction(true) (#1072)
Some transaction globals were leaked when calling `newrelic_end_transaction(true)`. The freeing of these transaction globals has been moved to join all the other transaction globals to alleviate this. In so doing, additional rshutdown methods are needed. This is because the transaction globals contain reference incremented zvals. If we wait until postdeactivate, those zvals will be in an undefined state and the data structures containing those references cannot be properly freed. In line with other transaction globals, we trigger a shutdown method during rshutdown to free these globals while the zvals are still valid.
1 parent 469e63e commit 5d16756

File tree

10 files changed

+469
-3
lines changed

10 files changed

+469
-3
lines changed

agent/php_mysqli.c

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -688,3 +688,28 @@ const char* nr_php_mysqli_strip_persistent_prefix(const char* host) {
688688

689689
return host;
690690
}
691+
692+
void nr_php_mysqli_rshutdown() {
693+
/*
694+
* This frees mysqli metadata stored in the transaction.
695+
*
696+
* `mysqli_queries` contains duplicates of zvals. If
697+
* `nr_php_txn_end` is called from the post-deactivate callback, request
698+
* shutdown functions have already been called; and the Zend VM has already
699+
* forcefully freed all dangling zvals that are not referenced by the global
700+
* scope (regardless of their reference count), thus leaving the zvals stored
701+
* in the mysqli_queries metadata in an "undefined" state. Consequently,
702+
* freeing the zvals in `nr_php_txn_end` at this stage can result in undefined
703+
* behavior.
704+
*
705+
* Calling this function during the RSHUTDOWN phase ensures that the zvals in
706+
* `mysqli_queries` are cleaned up before Zend winds down the VM and
707+
* forcefully frees zvals.
708+
*
709+
* If `nr_php_txn_end` is called outside the post-deactivate callback,
710+
* it frees `mysqli_queries` by itself.
711+
*/
712+
if (nrlikely(NRPRG(txn))) {
713+
nr_hashmap_destroy(&NRTXNGLOBAL(mysqli_queries));
714+
}
715+
}

agent/php_mysqli.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,4 +163,10 @@ extern nr_datastore_instance_t* nr_php_mysqli_retrieve_datastore_instance(
163163
extern void nr_php_mysqli_remove_datastore_instance(
164164
const zval* mysqli_obj TSRMLS_DC);
165165

166+
/*
167+
* Purpose : Frees reference incremented, transaction global zvals
168+
* that must be cleaned up prior to postdeactivate
169+
*/
170+
extern void nr_php_mysqli_rshutdown();
171+
166172
#endif /* PHP_MYSQLI_HDR */

agent/php_pdo.c

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -638,3 +638,28 @@ zval* nr_php_pdo_disable_persistence(const zval* options TSRMLS_DC) {
638638
nr_php_zval_free(&persistent);
639639
return result;
640640
}
641+
642+
void nr_php_pdo_rshutdown() {
643+
/*
644+
* This frees pdo metadata stored in the transaction.
645+
*
646+
* `pdo_link_options` contains duplicates of zvals. If
647+
* `nr_php_txn_end` is called from the post-deactivate callback, request
648+
* shutdown functions have already been called; and the Zend VM has already
649+
* forcefully freed all dangling zvals that are not referenced by the global
650+
* scope (regardless of their reference count), thus leaving the zvals stored
651+
* in the pdo_link_options metadata in an "undefined" state. Consequently,
652+
* freeing the zvals in `nr_php_txn_end` at this stage can result in undefined
653+
* behavior.
654+
*
655+
* Calling this function during the RSHUTDOWN phase ensures that the zvals in
656+
* `pdo_link_options` are cleaned up before Zend winds down the VM and
657+
* forcefully frees zvals.
658+
*
659+
* If `nr_php_txn_end` is called outside the post-deactivate callback,
660+
* it frees `pdo_link_options` by itself.
661+
*/
662+
if (nrlikely(NRPRG(txn))) {
663+
nr_hashmap_destroy(&NRTXNGLOBAL(pdo_link_options));
664+
}
665+
}

agent/php_pdo.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,4 +161,9 @@ extern nr_status_t nr_php_pdo_parse_data_source(
161161
extern void nr_php_pdo_free_data_sources(struct pdo_data_src_parser* parsed,
162162
size_t nparams);
163163

164+
/*
165+
* Purpose : Frees reference incremented, transaction global zvals
166+
* that must be cleaned up prior to postdeactivate
167+
*/
168+
extern void nr_php_pdo_rshutdown();
164169
#endif

agent/php_rshutdown.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
#include "php_globals.h"
1313
#include "php_user_instrument.h"
1414
#include "php_wrapper.h"
15+
#include "php_mysqli.h"
16+
#include "php_pdo.h"
1517
#include "util_logging.h"
1618
#include "lib_guzzle4.h"
1719

@@ -49,6 +51,8 @@ PHP_RSHUTDOWN_FUNCTION(newrelic) {
4951

5052
nr_guzzle4_rshutdown(TSRMLS_C);
5153
nr_curl_rshutdown(TSRMLS_C);
54+
nr_php_pdo_rshutdown();
55+
nr_php_mysqli_rshutdown();
5256

5357
nrl_verbosedebug(NRL_INIT, "RSHUTDOWN processing done");
5458

agent/php_txn.c

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1191,9 +1191,6 @@ static void nr_php_txn_do_shutdown(nrtxn_t* txn TSRMLS_DC) {
11911191
* cannot be configured into the browser client config.
11921192
*/
11931193
nr_php_capture_request_parameters(txn TSRMLS_CC);
1194-
1195-
nr_hashmap_destroy(&NRTXNGLOBAL(mysqli_queries));
1196-
nr_hashmap_destroy(&NRTXNGLOBAL(pdo_link_options));
11971194
}
11981195

11991196
void nr_php_txn_shutdown(TSRMLS_D) {
@@ -1360,6 +1357,9 @@ nr_status_t nr_php_txn_end(int ignoretxn, int in_post_deactivate TSRMLS_DC) {
13601357
nr_hashmap_destroy(&NRTXNGLOBAL(curl_multi_metadata));
13611358

13621359
nr_mysqli_metadata_destroy(&NRTXNGLOBAL(mysqli_links));
1360+
nr_hashmap_destroy(&NRTXNGLOBAL(mysqli_queries));
1361+
1362+
nr_hashmap_destroy(&NRTXNGLOBAL(pdo_link_options));
13631363

13641364
return NR_SUCCESS;
13651365
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<?php
2+
/*
3+
* Copyright 2020 New Relic Corporation. All rights reserved.
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
/*DESCRIPTION
8+
Tests that transaction globals are properly freed when using New Relic API
9+
*/
10+
11+
/*SKIPIF
12+
<?php
13+
if (!extension_loaded("curl")) {
14+
die("skip: curl extension required");
15+
}
16+
*/
17+
18+
/*INI
19+
newrelic.transaction_tracer.threshold=0
20+
newrelic.cross_application_tracer.enabled = false
21+
newrelic.distributed_tracing_enabled=0
22+
*/
23+
24+
/*EXPECT
25+
X-NewRelic-ID=missing X-NewRelic-Transaction=missing tracing endpoint reached
26+
X-NewRelic-ID=missing X-NewRelic-Transaction=missing tracing endpoint reached
27+
ok - end of function reached without crash
28+
*/
29+
30+
31+
/* the following metrics would be expected as well but due to an issue on the creation
32+
* of these metrics when a transaction is stopped and new one started they do not
33+
* currently show up.
34+
* [{"name": "Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]],
35+
* [{"name": "Supportability/Logging/Metrics/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]]
36+
*/
37+
38+
/*EXPECT_METRICS
39+
[
40+
"?? agent run id",
41+
"?? start time",
42+
"?? stop time",
43+
[
44+
[{"name":"External/all"}, [1, "??", "??", "??", "??", "??"]],
45+
[{"name":"External/allOther"}, [1, "??", "??", "??", "??", "??"]],
46+
[{"name":"External/127.0.0.1/all"}, [1, "??", "??", "??", "??", "??"]],
47+
[{"name":"External/127.0.0.1/all",
48+
"scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]],
49+
[{"name":"OtherTransaction/all"}, [1, "??", "??", "??", "??", "??"]],
50+
[{"name":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]],
51+
[{"name":"OtherTransactionTotalTime"}, [1, "??", "??", "??", "??", "??"]],
52+
[{"name":"OtherTransactionTotalTime/php__FILE__"}, [1, "??", "??", "??", "??", "??"]],
53+
[{"name":"Supportability/api/start_transaction"}, [1, "??", "??", "??", "??", "??"]],
54+
[{"name":"Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]],
55+
[{"name":"Supportability/Logging/Metrics/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]],
56+
[{"name":"Supportability/Logging/LocalDecorating/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]],
57+
[{"name":"Supportability/Logging/Labels/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]]
58+
]
59+
]
60+
*/
61+
62+
63+
64+
65+
require_once(realpath(dirname(__FILE__)) . '/../../../include/tap.php');
66+
require_once(realpath(dirname(__FILE__)) . '/../../../include/config.php');
67+
68+
function test_txn_restart() {
69+
$url = make_tracing_url(realpath(dirname(__FILE__)) . '/../../../include/tracing_endpoint.php');
70+
71+
$ch1 = curl_init($url);
72+
$ch2 = curl_init($url);
73+
$mh = curl_multi_init();
74+
75+
$active = 0;
76+
77+
curl_multi_add_handle($mh, $ch1);
78+
curl_multi_exec($mh, $active);
79+
80+
newrelic_ignore_transaction();
81+
newrelic_end_transaction();
82+
newrelic_start_transaction(ini_get("newrelic.appname"));
83+
84+
curl_multi_add_handle($mh, $ch2);
85+
do {
86+
curl_multi_exec($mh, $active);
87+
} while ($active > 0);
88+
89+
curl_close($ch1);
90+
curl_close($ch2);
91+
curl_multi_close($mh);
92+
93+
tap_ok("end of function reached without crash", true);
94+
}
95+
96+
test_txn_restart();
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<?php
2+
/*
3+
* Copyright 2020 New Relic Corporation. All rights reserved.
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
/*DESCRIPTION
8+
Test that transaction globals are properly freed when using New Relic API
9+
*/
10+
11+
/*SKIPIF
12+
<?php
13+
require("skipif.inc");
14+
*/
15+
16+
/*INI
17+
*/
18+
19+
/*EXPECT
20+
traceparent=found tracestate=found newrelic=found X-NewRelic-ID=missing X-NewRelic-Transaction=missing tracing endpoint reached
21+
traceparent=found tracestate=found newrelic=found X-NewRelic-ID=missing X-NewRelic-Transaction=missing tracing endpoint reached
22+
traceparent=found tracestate=found newrelic=found X-NewRelic-ID=missing X-NewRelic-Transaction=missing Customer-Header=found tracing endpoint reached
23+
traceparent=found tracestate=found newrelic=found X-NewRelic-ID=missing X-NewRelic-Transaction=missing tracing endpoint reached
24+
traceparent=found tracestate=found newrelic=found X-NewRelic-ID=missing X-NewRelic-Transaction=missing tracing endpoint reached
25+
traceparent=found tracestate=found newrelic=found X-NewRelic-ID=missing X-NewRelic-Transaction=missing Customer-Header=found tracing endpoint reached
26+
*/
27+
28+
/*EXPECT_RESPONSE_HEADERS
29+
*/
30+
31+
/*EXPECT_METRICS
32+
[
33+
"?? agent run id",
34+
"?? timeframe start",
35+
"?? timeframe stop",
36+
[
37+
[{"name":"External/127.0.0.1/all"}, [3, "??", "??", "??", "??", "??"]],
38+
[{"name":"External/127.0.0.1/all",
39+
"scope":"OtherTransaction/php__FILE__"}, [3, "??", "??", "??", "??", "??"]],
40+
[{"name":"External/all"}, [3, "??", "??", "??", "??", "??"]],
41+
[{"name":"External/allOther"}, [3, "??", "??", "??", "??", "??"]],
42+
[{"name":"OtherTransaction/all"}, [1, "??", "??", "??", "??", "??"]],
43+
[{"name":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]],
44+
[{"name":"OtherTransactionTotalTime"}, [1, "??", "??", "??", "??", "??"]],
45+
[{"name":"OtherTransactionTotalTime/php__FILE__"}, [1, "??", "??", "??", "??", "??"]],
46+
[{"name":"Supportability/api/start_transaction"}, [1, "??", "??", "??", "??", "??"]],
47+
[{"name":"Supportability/api/end_transaction"}, [1, "??", "??", "??", "??", "??"]],
48+
[{"name":"Supportability/PHP/package/guzzlehttp/guzzle/7/detected"}, [1, "??", "??", "??", "??", "??"]],
49+
[{"name":"Supportability/Unsupported/curl_setopt/CURLOPT_HEADERFUNCTION/closure"}, [3, 0, 0, 0, 0, 0]],
50+
[{"name":"DurationByCaller/Unknown/Unknown/Unknown/Unknown/all"}, [1, "??", "??", "??", "??", "??"]],
51+
[{"name":"DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther"}, [1, "??", "??", "??", "??", "??"]],
52+
[{"name":"Supportability/TraceContext/Create/Success"}, [3, "??", "??", "??", "??", "??"]],
53+
[{"name":"Supportability/DistributedTrace/CreatePayload/Success"}, [3, "??", "??", "??", "??", "??"]],
54+
[{"name":"Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]],
55+
[{"name":"Supportability/Logging/Metrics/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]],
56+
[{"name":"Supportability/Logging/LocalDecorating/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]],
57+
[{"name":"Supportability/Logging/Labels/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]]
58+
]
59+
]
60+
*/
61+
62+
63+
?>
64+
<?php
65+
require_once(realpath(dirname(__FILE__)) . '/../../../include/config.php');
66+
require_once(realpath(dirname(__FILE__)) . '/../../../include/unpack_guzzle.php');
67+
require_guzzle(7);
68+
69+
use GuzzleHttp\Client;
70+
71+
function run_test() {
72+
/* Create URL. */
73+
$url = "http://" . make_tracing_url(realpath(dirname(__FILE__)) . '/../../../include/tracing_endpoint.php');
74+
75+
$client = new Client();
76+
$response = $client->get($url);
77+
echo $response->getBody();
78+
79+
$response = $client->get($url, [
80+
'headers' => [
81+
'zip' => 'zap']]);
82+
echo $response->getBody();
83+
84+
$response = $client->get($url, [
85+
'headers' => [
86+
'zip' => 'zap',
87+
CUSTOMER_HEADER => 'zap']]);
88+
echo $response->getBody();
89+
}
90+
91+
run_test();
92+
93+
newrelic_end_transaction(true);
94+
newrelic_start_transaction(ini_get("newrelic.appname"));
95+
96+
run_test();
97+
98+
newrelic_end_transaction();

0 commit comments

Comments
 (0)