Skip to content

Commit

Permalink
Add infinite loop detection (#85)
Browse files Browse the repository at this point in the history
* Add infinite loop detection

* Optimize test.php

* Optimize add_executed_opline_num

* Add tests

* Fix test

* Set environment USE_ZEND_ALLOC=0
  • Loading branch information
huanghantao authored Jan 31, 2021
1 parent 67bc1d2 commit 45eaa7b
Show file tree
Hide file tree
Showing 10 changed files with 179 additions and 1 deletion.
9 changes: 9 additions & 0 deletions include/context.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@

namespace yasd {

class CurrentFunctionStatus {
public:
int executed_opline_num = 0;

CurrentFunctionStatus() {}
~CurrentFunctionStatus() {}
};

class StackFrame {
public:
std::string filename;
Expand All @@ -41,6 +49,7 @@ class Context {
int64_t next_level = 0;

std::vector<StackFrame *> *strace = nullptr;
std::vector<CurrentFunctionStatus *> function_status;

Context();
~Context();
Expand Down
1 change: 1 addition & 0 deletions php_yasd.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ ZEND_BEGIN_MODULE_GLOBALS(yasd)
uint16_t remote_port;
uint16_t depth;
int log_level;
int max_executed_opline_num;

// compatible with phpstorm
int coverage_enable;
Expand Down
17 changes: 17 additions & 0 deletions src/base.cc
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,21 @@ void clear_watch_point(zend_execute_data *execute_data) {
}
}

yasd::CurrentFunctionStatus *save_current_function_status() {
yasd::Context *context = global->get_current_context();

yasd::CurrentFunctionStatus *current_function_status = new yasd::CurrentFunctionStatus();
context->function_status.emplace_back(current_function_status);
return current_function_status;
}

void drop_current_function_status(yasd::CurrentFunctionStatus *function_status) {
yasd::Context *context = global->get_current_context();

context->function_status.pop_back();
delete function_status;
}

void yasd_execute_ex(zend_execute_data *execute_data) {
// if not set -e, we will not initialize global
if (!(CG(compiler_options) & ZEND_COMPILE_EXTENDED_INFO)) {
Expand All @@ -128,7 +143,9 @@ void yasd_execute_ex(zend_execute_data *execute_data) {

context->level++;
yasd::StackFrame *frame = save_prev_stack_frame(execute_data);
yasd::CurrentFunctionStatus *function_status = save_current_function_status();
old_execute_ex(execute_data);
drop_current_function_status(function_status);
// reduce the function call trace
drop_prev_stack_frame(frame);
// reduce the level of function call
Expand Down
14 changes: 14 additions & 0 deletions test.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ function test3() {
];
}

// Suppose the business code calls a function to determine whether to end the loop,
// but the business code always returns false
function test4() {
return false;
}

class Foo3
{
public $a;
Expand Down Expand Up @@ -123,6 +129,14 @@ public function __construct($a)
}
}

$a = 0;
while (true) {
$a++;
if (test4() || $a > 50000) {
break;
}
}

echo 'hello' . PHP_EOL;

require __DIR__ . '/header.php';
Expand Down
2 changes: 1 addition & 1 deletion tests/start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ else
fi

if [ $? -eq 0 ]; then
PHPT=1 ${TEST_PHP_EXECUTABLE} -d "memory_limit=1024m" ${__DIR__}/run-tests ${glob}
USE_ZEND_ALLOC=0 PHPT=1 ${TEST_PHP_EXECUTABLE} -d "memory_limit=1024m" ${__DIR__}/run-tests ${glob}
fi

# after tests
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

// Suppose the business code calls a function to determine whether to end the loop,
// but the business code always returns false
function test() {
return false;
}

$a = 0;
while (true) {
$a++;
if (test() || $a > 100000) {
break;
}
}
?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
--TEST--
infinite_loop_detection: for_loop
--SKIPIF--
--FILE--
<?php

use Yasd\DbgpClient;

require dirname(__DIR__, 3) . '/Include/bootstrap.php';

$basename = basename(__FILE__, '.php');
$filename = realpath(dirname(__FILE__) . "/{$basename}.inc");

$commands = [
'run',
'stop',
];

$client = (new DbgpClient())->setCommands($commands)->setTestFile($filename)->start();

?>
--EXPECTF--
<?xml version="1.0" encoding="iso-8859-1"?>
<init xmlns="urn:debugger_protocol_v1" xmlns:xdebug="https://xdebug.org/dbgp/xdebug" fileuri=%s language="PHP" xdebug:language_version=%s protocol_version="1.0" appid=%s idekey=%s><engine version=%s><![CDATA[Yasd]]></engine><author><![CDATA[Codinghuang]]></author><url><![CDATA[https://github.com/swoole/yasd]]></url><copyright><![CDATA[Copyright (c) 2020-2021 by Codinghuang]]></copyright></init>

-> run -i 1
<?xml version="1.0" encoding="iso-8859-1"?>
<response xmlns="urn:debugger_protocol_v1" xmlns:xdebug="https://xdebug.org/dbgp/xdebug" command="run" transaction_id="1" status="break" reason="ok"><xdebug:message filename="file://%s" lineno="%d"/></response>

-> stop -i 2
<?xml version="1.0" encoding="iso-8859-1"?>
<response xmlns="urn:debugger_protocol_v1" xmlns:xdebug="https://xdebug.org/dbgp/xdebug" command="stop" transaction_id="2" status="stopped" reason="ok"/>
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

// Suppose the business code calls a function to determine whether to end the loop,
// but the business code always returns false
function test() {
return false;
}

$a = 0;

LABEL:

$a++;
if (test() || $a > 100000) {
goto END;
}

goto LABEL;

END:
?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
--TEST--
infinite_loop_detection: goto_label_loop
--SKIPIF--
--FILE--
<?php

use Yasd\DbgpClient;

require dirname(__DIR__, 3) . '/Include/bootstrap.php';

$basename = basename(__FILE__, '.php');
$filename = realpath(dirname(__FILE__) . "/{$basename}.inc");

$commands = [
'run',
'stop',
];

$client = (new DbgpClient())->setCommands($commands)->setTestFile($filename)->start();

?>
--EXPECTF--
<?xml version="1.0" encoding="iso-8859-1"?>
<init xmlns="urn:debugger_protocol_v1" xmlns:xdebug="https://xdebug.org/dbgp/xdebug" fileuri=%s language="PHP" xdebug:language_version=%s protocol_version="1.0" appid=%s idekey=%s><engine version=%s><![CDATA[Yasd]]></engine><author><![CDATA[Codinghuang]]></author><url><![CDATA[https://github.com/swoole/yasd]]></url><copyright><![CDATA[Copyright (c) 2020-2021 by Codinghuang]]></copyright></init>

-> run -i 1
<?xml version="1.0" encoding="iso-8859-1"?>
<response xmlns="urn:debugger_protocol_v1" xmlns:xdebug="https://xdebug.org/dbgp/xdebug" command="run" transaction_id="1" status="break" reason="ok"><xdebug:message filename="file://%s" lineno="%d"/></response>

-> stop -i 2
<?xml version="1.0" encoding="iso-8859-1"?>
<response xmlns="urn:debugger_protocol_v1" xmlns:xdebug="https://xdebug.org/dbgp/xdebug" command="stop" transaction_id="2" status="stopped" reason="ok"/>
36 changes: 36 additions & 0 deletions yasd.cc
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ STD_PHP_INI_ENTRY("yasd.depth", "1", PHP_INI_ALL, OnUpdateLong,
depth, zend_yasd_globals, yasd_globals)
STD_PHP_INI_ENTRY("yasd.log_level", "-1", PHP_INI_ALL, OnUpdateLong,
log_level, zend_yasd_globals, yasd_globals)
STD_PHP_INI_ENTRY("yasd.max_executed_opline_num", "10000", PHP_INI_ALL, OnUpdateLong,
max_executed_opline_num, zend_yasd_globals, yasd_globals)

// compatible with phpstorm
STD_PHP_INI_ENTRY("xdebug.coverage_enable", "1", PHP_INI_ALL, OnUpdateLong,
Expand Down Expand Up @@ -176,6 +178,32 @@ bool is_hit_line_condition_breakpoint(int lineno) {
return Z_TYPE(retval) == IS_TRUE;
}

void add_executed_opline_num(zend_execute_data *frame) {
yasd::Context *context = global->get_current_context();
const zend_op *opline = frame->opline + 1;

// skip ZEND_NOP, every function definition, the location of the class definition, is a ZEND_NOP
if (opline->opcode == ZEND_NOP) {
return;
}

yasd::CurrentFunctionStatus *function_status = context->function_status.back();
function_status->executed_opline_num++;
}

bool is_infinite_loop() {
yasd::Context *context = global->get_current_context();

yasd::CurrentFunctionStatus *function_status = context->function_status.back();

if (function_status->executed_opline_num < YASD_G(max_executed_opline_num)) {
return false;
}

function_status->executed_opline_num = 0;
return true;
}

ZEND_DLEXPORT void yasd_statement_call(zend_execute_data *frame) {
// zend_op_array *op_array = &frame->func->op_array;
const zend_op *online = EG(current_execute_data)->opline;
Expand All @@ -184,6 +212,8 @@ ZEND_DLEXPORT void yasd_statement_call(zend_execute_data *frame) {
int start_lineno;

yasd::Context *context = global->get_current_context();

add_executed_opline_num(frame);

if (!EG(current_execute_data)) {
return;
Expand All @@ -206,6 +236,12 @@ ZEND_DLEXPORT void yasd_statement_call(zend_execute_data *frame) {
}
}

// infinite loop detection
if (is_infinite_loop()) {
return global->debugger->handle_request(filename, lineno);
}

// The breakpoint check should be last
if (!global->do_step && !global->do_next) {
if (!is_hit_line_breakpoint(filename, lineno)) {
return;
Expand Down

0 comments on commit 45eaa7b

Please sign in to comment.