From 7963580d72a358975133f86f01de2d2eab08ba38 Mon Sep 17 00:00:00 2001 From: xXx-caillou-xXx Date: Fri, 13 Jul 2018 10:36:50 +0200 Subject: Massively optimize how rules are handled This commit does a lot of things: - Use hashtables instead of lists to store the rules - Rules that can be applied at launch time won't be tried at runtime - Improve feedback when writing nonsensical rules - Make intensive use of `zend_string` instead of `char*`--- src/sp_disabled_functions.c | 304 ++++++++++++++++++++++++++------------------ 1 file changed, 180 insertions(+), 124 deletions(-) (limited to 'src/sp_disabled_functions.c') diff --git a/src/sp_disabled_functions.c b/src/sp_disabled_functions.c index 341c0a4..14783f6 100644 --- a/src/sp_disabled_functions.c +++ b/src/sp_disabled_functions.c @@ -65,7 +65,7 @@ static bool is_functions_list_matching(zend_execute_data* execute_data, static bool is_local_var_matching( zend_execute_data* execute_data, const sp_disabled_function* const config_node) { - zval* var_value; + zval* var_value = {0}; var_value = sp_get_var_value(execute_data, config_node->var, false); if (var_value) { @@ -76,14 +76,13 @@ static bool is_local_var_matching( return true; } } else if (sp_match_array_value(var_value, config_node->value, - config_node->value_r)) { + config_node->r_value)) { return true; } } else { - char* var_value_str = sp_convert_to_string(var_value); + const zend_string* var_value_str = sp_zval_to_zend_string(var_value); bool match = sp_match_value(var_value_str, config_node->value, - config_node->value_r); - efree(var_value_str); + config_node->r_value); if (true == match) { return true; @@ -93,28 +92,12 @@ static bool is_local_var_matching( return false; } -static inline const sp_list_node* get_config_node(const char* builtin_name) { - if (EXPECTED(!builtin_name)) { - return SNUFFLEUPAGUS_G(config) - .config_disabled_functions->disabled_functions; - } else if (!strcmp(builtin_name, "eval")) { - return SNUFFLEUPAGUS_G(config).config_disabled_constructs->construct_eval; - } else if (!strcmp(builtin_name, "include") || - !strcmp(builtin_name, "include_once") || - !strcmp(builtin_name, "require") || - !strcmp(builtin_name, "require_once")) { - return SNUFFLEUPAGUS_G(config) - .config_disabled_constructs->construct_include; - } - ZEND_ASSUME(0); -} - static bool is_param_matching(zend_execute_data* execute_data, sp_disabled_function const* const config_node, - const char* builtin_name, - const char* builtin_param, const char** arg_name, + const zend_string* builtin_param, + const char** arg_name, const char* builtin_param_name, - const char** arg_value_str) { + const zend_string** arg_value_str) { int nb_param = ZEND_CALL_NUM_ARGS(execute_data); int i = 0; zval* arg_value; @@ -136,14 +119,14 @@ static bool is_param_matching(zend_execute_data* execute_data, } } - if (builtin_name) { + if (builtin_param) { /* We're matching on a language construct (here named "builtin"), * and they can only take a single argument, but PHP considers them * differently than functions arguments. */ *arg_name = builtin_param_name; *arg_value_str = builtin_param; return sp_match_value(builtin_param, config_node->value, - config_node->value_r); + config_node->r_value); } else if (config_node->r_param || config_node->pos != -1) { // We're matching on a function (and not a language construct) for (; i < nb_param; i++) { @@ -165,20 +148,20 @@ static bool is_param_matching(zend_execute_data* execute_data, return true; } } else if (Z_TYPE_P(arg_value) == IS_ARRAY) { - *arg_value_str = sp_convert_to_string(arg_value); + *arg_value_str = sp_zval_to_zend_string(arg_value); if (config_node->key || config_node->r_key) { if (sp_match_array_key(arg_value, config_node->key, config_node->r_key)) { return true; } } else if (sp_match_array_value(arg_value, config_node->value, - config_node->value_r)) { + config_node->r_value)) { return true; } } else { - *arg_value_str = sp_convert_to_string(arg_value); + *arg_value_str = sp_zval_to_zend_string(arg_value); if (sp_match_value(*arg_value_str, config_node->value, - config_node->value_r)) { + config_node->r_value)) { return true; } } @@ -189,7 +172,7 @@ static bool is_param_matching(zend_execute_data* execute_data, arg_value = sp_get_var_value(execute_data, config_node->param, true); if (arg_value) { - *arg_value_str = sp_convert_to_string(arg_value); + *arg_value_str = sp_zval_to_zend_string(arg_value); if (config_node->param_type) { // Are we matching on the `type`? if (config_node->param_type == Z_TYPE_P(arg_value)) { return true; @@ -201,11 +184,11 @@ static bool is_param_matching(zend_execute_data* execute_data, return true; } } else if (sp_match_array_value(arg_value, config_node->value, - config_node->value_r)) { + config_node->r_value)) { return true; } } else if (sp_match_value(*arg_value_str, config_node->value, - config_node->value_r)) { + config_node->r_value)) { return true; } } @@ -216,7 +199,7 @@ static bool is_param_matching(zend_execute_data* execute_data, static zend_execute_data* is_file_matching( zend_execute_data* const execute_data, sp_disabled_function const* const config_node, - char const* const current_filename) { + zend_string const* const current_filename) { #define ITERATE(ex) \ ex = ex->prev_execute_data; \ while (ex && (!ex->func || !ZEND_USER_CODE(ex->func->type))) \ @@ -225,22 +208,21 @@ static zend_execute_data* is_file_matching( zend_execute_data* ex = execute_data; if (config_node->filename) { - if (0 == strcmp(current_filename, config_node->filename)) { + if (zend_string_equals_literal(current_filename, config_node->filename)) { return ex; } ITERATE(ex); - if (0 == - strcmp(ZSTR_VAL(ex->func->op_array.filename), config_node->filename)) { + if (zend_string_equals_literal(ex->func->op_array.filename, + config_node->filename)) { return ex; } } else if (config_node->r_filename) { - if (true == - sp_is_regexp_matching(config_node->r_filename, current_filename)) { + if (sp_is_regexp_matching_zend(config_node->r_filename, current_filename)) { return ex; } ITERATE(ex); - if (true == sp_is_regexp_matching(config_node->r_filename, - ZSTR_VAL(ex->func->op_array.filename))) { + if (sp_is_regexp_matching_zend(config_node->r_filename, + ex->func->op_array.filename)) { return ex; } } @@ -251,10 +233,10 @@ static zend_execute_data* is_file_matching( static bool check_is_builtin_name( sp_disabled_function const* const config_node) { if (config_node->function) { - return (!strcmp(config_node->function, "include") || - !strcmp(config_node->function, "include_once") || - !strcmp(config_node->function, "require") || - !strcmp(config_node->function, "require_once")); + return (zend_string_equals_literal(config_node->function, "include") || + zend_string_equals_literal(config_node->function, "include_once") || + zend_string_equals_literal(config_node->function, "require") || + zend_string_equals_literal(config_node->function, "require_once")); } if (config_node->r_function) { return (sp_is_regexp_matching(config_node->r_function, "include") || @@ -265,43 +247,67 @@ static bool check_is_builtin_name( return false; } -bool should_disable(zend_execute_data* execute_data, const char* builtin_name, - const char* builtin_param, const char* builtin_param_name) { - char current_file_hash[SHA256_SIZE * 2 + 1] = {0}; - const sp_list_node* config = get_config_node(builtin_name); - char* complete_path_function = NULL; - const char* current_filename = NULL; - unsigned int line = 0; - char* filename = NULL; +bool should_disable_ht(zend_execute_data* execute_data, + const char* builtin_name, + const zend_string* builtin_param, + const char* builtin_param_name, + const sp_list_node* config, const HashTable* ht) { + char* complete_function_path = NULL; + const sp_list_node* ht_entry = NULL; + bool ret = false; + zend_string* current_filename; if (!execute_data) { return false; } - if (!config || !config->data) { - return false; + if (builtin_name) { + complete_function_path = estrdup(builtin_name); + } else { + complete_function_path = get_complete_function_path(execute_data); + if (!complete_function_path) { + return false; + } } - if (UNEXPECTED(builtin_name && !strcmp(builtin_name, "eval"))) { + if (UNEXPECTED(builtin_param && !strcmp(complete_function_path, "eval"))) { current_filename = get_eval_filename(zend_get_executed_filename()); } else { - current_filename = zend_get_executed_filename(); + const char* tmp = zend_get_executed_filename(); + current_filename = zend_string_init(tmp, strlen(tmp), 0); } - complete_path_function = get_complete_function_path(execute_data); - if (!complete_path_function) { - if (builtin_name) { - complete_path_function = estrdup(builtin_name); - } else { - return false; - } + ht_entry = zend_hash_str_find_ptr(ht, complete_function_path, + strlen(complete_function_path)); + + if (ht_entry && + should_disable(execute_data, complete_function_path, builtin_param, + builtin_param_name, ht_entry, current_filename)) { + ret = true; + } else if (config && config->data) { + ret = should_disable(execute_data, complete_function_path, builtin_param, + builtin_param_name, config, current_filename); } + efree(complete_function_path); + efree(current_filename); + return ret; +} + +bool should_disable(zend_execute_data* execute_data, + const char* complete_function_path, + const zend_string* builtin_param, + const char* builtin_param_name, const sp_list_node* config, + const zend_string* current_filename) { + char current_file_hash[SHA256_SIZE * 2 + 1] = {0}; + unsigned int line = 0; + char* filename = NULL; + while (config) { sp_disabled_function const* const config_node = (sp_disabled_function*)(config->data); const char* arg_name = NULL; - const char* arg_value_str = NULL; + const zend_string* arg_value_str = NULL; /* The order matters, since when we have `config_node->functions_list`, we also do have `config_node->function` */ @@ -311,12 +317,13 @@ bool should_disable(zend_execute_data* execute_data, const char* builtin_name, goto next; } } else if (config_node->function) { - if (0 != strcmp(config_node->function, complete_path_function)) { + if (0 != + strcmp(ZSTR_VAL(config_node->function), complete_function_path)) { goto next; } } else if (config_node->r_function) { if (false == sp_is_regexp_matching(config_node->r_function, - complete_path_function)) { + complete_function_path)) { goto next; } } @@ -350,9 +357,10 @@ bool should_disable(zend_execute_data* execute_data, const char* builtin_name, if (config_node->hash) { if ('\0' == current_file_hash[0]) { - compute_hash(current_filename, current_file_hash); + compute_hash(ZSTR_VAL(current_filename), current_file_hash); } - if (0 != strncmp(current_file_hash, config_node->hash, SHA256_SIZE)) { + if (0 != strncmp(current_file_hash, ZSTR_VAL(config_node->hash), + SHA256_SIZE)) { goto next; } } @@ -360,25 +368,25 @@ bool should_disable(zend_execute_data* execute_data, const char* builtin_name, /* Check if we filter on parameter value*/ if (config_node->param || config_node->r_param || (config_node->pos != -1)) { - if (!builtin_name && execute_data->func->op_array.arg_info->is_variadic) { + if (!builtin_param && + execute_data->func->op_array.arg_info->is_variadic) { sp_log_err( "disable_function", "Snuffleupagus doesn't support variadic functions yet, sorry. " "Check https://github.com/nbs-system/snuffleupagus/issues/164 for " "details."); - } else if (false == is_param_matching(execute_data, config_node, - builtin_name, builtin_param, - &arg_name, builtin_param_name, - &arg_value_str)) { + } else if (false == is_param_matching( + execute_data, config_node, builtin_param, + &arg_name, builtin_param_name, &arg_value_str)) { goto next; } } - if (config_node->value_r || config_node->value) { + if (config_node->r_value || config_node->value) { if (check_is_builtin_name(config_node)) { - if (false == is_param_matching(execute_data, config_node, builtin_name, - builtin_param, &arg_name, - builtin_param_name, &arg_value_str)) { + if (false == is_param_matching(execute_data, config_node, builtin_param, + &arg_name, builtin_param_name, + &arg_value_str)) { goto next; } } @@ -390,63 +398,76 @@ bool should_disable(zend_execute_data* execute_data, const char* builtin_name, } if (config_node->functions_list) { - sp_log_disable(config_node->function, arg_name, arg_value_str, + sp_log_disable(ZSTR_VAL(config_node->function), arg_name, arg_value_str, config_node, line, filename); } else { - sp_log_disable(complete_path_function, arg_name, arg_value_str, + sp_log_disable(complete_function_path, arg_name, arg_value_str, config_node, line, filename); } if (true == config_node->simulation) { goto next; } else { // We've got a match, the function won't be executed - efree(complete_path_function); return true; } next: config = config->next; } allow: - efree(complete_path_function); return false; } -bool should_drop_on_ret(zval* return_value, - const zend_execute_data* const execute_data) { - const sp_list_node* config = - SNUFFLEUPAGUS_G(config).config_disabled_functions_ret->disabled_functions; - char* complete_path_function = get_complete_function_path(execute_data); - const char* current_filename = zend_get_executed_filename(TSRMLS_C); - char current_file_hash[SHA256_SIZE * 2 + 1] = {0}; - bool match_type = false, match_value = false; +bool should_drop_on_ret_ht(zval* return_value, + const zend_execute_data* const execute_data, + const sp_list_node* config, const HashTable* ht) { + char* complete_function_path = get_complete_function_path(execute_data); + const sp_list_node* ht_entry = NULL; + bool ret = false; - if (!complete_path_function) { - return false; + if (!complete_function_path) { + return ret; } - if (!config || !config->data) { - return false; + ht_entry = zend_hash_str_find_ptr(ht, complete_function_path, + strlen(complete_function_path)); + + if (ht_entry && + should_drop_on_ret(return_value, ht_entry, complete_function_path)) { + ret = true; + } else if (config && config->data) { + ret = should_drop_on_ret(return_value, config, complete_function_path); } + efree(complete_function_path); + return ret; +} + +bool should_drop_on_ret(zval* return_value, const sp_list_node* config, + const char* complete_function_path) { + const char* current_filename = zend_get_executed_filename(TSRMLS_C); + char current_file_hash[SHA256_SIZE * 2 + 1] = {0}; + bool match_type = false, match_value = false; + while (config) { - char* ret_value_str = NULL; + const zend_string* ret_value_str = NULL; sp_disabled_function const* const config_node = (sp_disabled_function*)(config->data); assert(config_node->function || config_node->r_function); if (config_node->function) { - if (0 != strcmp(config_node->function, complete_path_function)) { + if (0 != + strcmp(ZSTR_VAL(config_node->function), complete_function_path)) { goto next; } } else if (config_node->r_function) { if (false == sp_is_regexp_matching(config_node->r_function, - complete_path_function)) { + complete_function_path)) { goto next; } } if (config_node->filename) { /* Check the current file name. */ - if (0 != strcmp(current_filename, config_node->filename)) { + if (0 != strcmp(current_filename, ZSTR_VAL(config_node->filename))) { goto next; } } else if (config_node->r_filename) { @@ -460,12 +481,13 @@ bool should_drop_on_ret(zval* return_value, if ('\0' == current_file_hash[0]) { compute_hash(current_filename, current_file_hash); } - if (0 != strncmp(current_file_hash, config_node->hash, SHA256_SIZE)) { + if (0 != strncmp(current_file_hash, ZSTR_VAL(config_node->hash), + SHA256_SIZE)) { goto next; } } - ret_value_str = sp_convert_to_string(return_value); + ret_value_str = sp_zval_to_zend_string(return_value); match_type = (config_node->ret_type) && (config_node->ret_type == Z_TYPE_P(return_value)); @@ -475,22 +497,16 @@ bool should_drop_on_ret(zval* return_value, if (true == match_type || true == match_value) { if (true == config_node->allow) { - efree(complete_path_function); - efree(ret_value_str); return false; } - sp_log_disable_ret(complete_path_function, ret_value_str, config_node); + sp_log_disable_ret(complete_function_path, ret_value_str, config_node); if (false == config_node->simulation) { - efree(complete_path_function); - efree(ret_value_str); return true; } } next: - efree(ret_value_str); config = config->next; } - efree(complete_path_function); return false; } @@ -498,7 +514,11 @@ ZEND_FUNCTION(check_disabled_function) { void (*orig_handler)(INTERNAL_FUNCTION_PARAMETERS); const char* current_function_name = get_active_function_name(TSRMLS_C); - if (true == should_disable(execute_data, NULL, NULL, NULL)) { + if (true == should_disable_ht( + execute_data, NULL, NULL, NULL, + SNUFFLEUPAGUS_G(config) + .config_disabled_functions_reg->disabled_functions, + SNUFFLEUPAGUS_G(config).config_disabled_functions_hooked)) { sp_terminate(); } @@ -506,23 +526,29 @@ ZEND_FUNCTION(check_disabled_function) { SNUFFLEUPAGUS_G(disabled_functions_hook), current_function_name, strlen(current_function_name)); orig_handler(INTERNAL_FUNCTION_PARAM_PASSTHRU); - if (true == should_drop_on_ret(return_value, execute_data)) { + if (true == + should_drop_on_ret_ht( + return_value, execute_data, + SNUFFLEUPAGUS_G(config) + .config_disabled_functions_reg_ret->disabled_functions, + SNUFFLEUPAGUS_G(config).config_disabled_functions_ret_hooked)) { sp_terminate(); } } -static int hook_functions(const sp_list_node* config) { +static int hook_functions_regexp(const sp_list_node* config) { while (config && config->data) { - const char* function_name = ((sp_disabled_function*)config->data)->function; + const zend_string* function_name = + ((sp_disabled_function*)config->data)->function; const sp_pcre* function_name_regexp = ((sp_disabled_function*)config->data)->r_function; assert(function_name || function_name_regexp); - if (NULL != function_name) { // hook function by name - HOOK_FUNCTION(function_name, disabled_functions_hook, + if (function_name) { + HOOK_FUNCTION(ZSTR_VAL(function_name), disabled_functions_hook, PHP_FN(check_disabled_function)); - } else if (NULL != function_name_regexp) { // hook function by regexp + } else { HOOK_FUNCTION_BY_REGEXP(function_name_regexp, disabled_functions_hook, PHP_FN(check_disabled_function)); } @@ -532,16 +558,36 @@ static int hook_functions(const sp_list_node* config) { return SUCCESS; } +static int hook_functions(HashTable* to_hook_ht, HashTable* hooked_ht) { + zend_string* key; + zval* value; + + ZEND_HASH_FOREACH_STR_KEY_VAL(to_hook_ht, key, value) { + if (!HOOK_FUNCTION(ZSTR_VAL(key), disabled_functions_hook, + PHP_FN(check_disabled_function)) || + check_is_builtin_name(((sp_list_node*)Z_PTR_P(value))->data)) { + zend_symtable_add_new(hooked_ht, key, value); + zend_hash_del(to_hook_ht, key); + } + } + ZEND_HASH_FOREACH_END(); + return SUCCESS; +} + ZEND_FUNCTION(eval_blacklist_callback) { void (*orig_handler)(INTERNAL_FUNCTION_PARAMETERS); const char* current_function_name = get_active_function_name(TSRMLS_C); + zend_string* tmp = + zend_string_init(current_function_name, strlen(current_function_name), 0); - if (true == check_is_in_eval_whitelist(current_function_name)) { + if (true == check_is_in_eval_whitelist(tmp)) { + zend_string_release(tmp); goto whitelisted; } + zend_string_release(tmp); if (SNUFFLEUPAGUS_G(in_eval) > 0) { - char* filename = get_eval_filename(zend_get_executed_filename()); + zend_string* filename = get_eval_filename(zend_get_executed_filename()); const int line_number = zend_get_executed_lineno(TSRMLS_C); if (SNUFFLEUPAGUS_G(config).config_eval->dump) { sp_log_request( @@ -549,14 +595,14 @@ ZEND_FUNCTION(eval_blacklist_callback) { SNUFFLEUPAGUS_G(config).config_eval->textual_representation, SP_TOKEN_EVAL_BLACKLIST); } - if (1 == SNUFFLEUPAGUS_G(config).config_eval->simulation) { + if (SNUFFLEUPAGUS_G(config).config_eval->simulation) { sp_log_msg("eval", SP_LOG_SIMULATION, "A call to %s was tried in eval, in %s:%d, logging it.", - current_function_name, filename, line_number); + current_function_name, ZSTR_VAL(filename), line_number); } else { sp_log_msg("eval", SP_LOG_DROP, "A call to %s was tried in eval, in %s:%d, dropping it.", - current_function_name, filename, line_number); + current_function_name, ZSTR_VAL(filename), line_number); sp_terminate(); } efree(filename); @@ -574,21 +620,31 @@ int hook_disabled_functions(void) { int ret = SUCCESS; + ret |= + hook_functions(SNUFFLEUPAGUS_G(config).config_disabled_functions, + SNUFFLEUPAGUS_G(config).config_disabled_functions_hooked); + ret |= hook_functions( - SNUFFLEUPAGUS_G(config).config_disabled_functions->disabled_functions); - ret |= hook_functions(SNUFFLEUPAGUS_G(config) - .config_disabled_functions_ret->disabled_functions); + SNUFFLEUPAGUS_G(config).config_disabled_functions_ret, + SNUFFLEUPAGUS_G(config).config_disabled_functions_ret_hooked); + + ret |= hook_functions_regexp( + SNUFFLEUPAGUS_G(config) + .config_disabled_functions_reg->disabled_functions); + + ret |= hook_functions_regexp( + SNUFFLEUPAGUS_G(config) + .config_disabled_functions_reg_ret->disabled_functions); if (NULL != SNUFFLEUPAGUS_G(config).config_eval->blacklist) { sp_list_node* it = SNUFFLEUPAGUS_G(config).config_eval->blacklist; while (it) { - hook_function((char*)it->data, + hook_function(ZSTR_VAL((zend_string*)it->data), SNUFFLEUPAGUS_G(sp_eval_blacklist_functions_hook), PHP_FN(eval_blacklist_callback)); it = it->next; } } - return ret; } -- cgit v1.3