diff options
| author | Ben Fuhrmannek | 2021-08-16 15:47:01 +0200 |
|---|---|---|
| committer | Ben Fuhrmannek | 2021-08-16 15:47:01 +0200 |
| commit | 5148ded7268b569fd5e720f90b44645c83ac3e9e (patch) | |
| tree | 9d5c3035a7a85ffc27de7c32b441994a21a6347a /src/sp_config.c | |
| parent | 9dc6b23a2219e809e665bac7d82567533751d39d (diff) | |
fincy new scanner/parser for config rules + fixed a few bugs along the way + fixed related unittests
Diffstat (limited to 'src/sp_config.c')
| -rw-r--r-- | src/sp_config.c | 305 |
1 files changed, 159 insertions, 146 deletions
diff --git a/src/sp_config.c b/src/sp_config.c index 37c749b..4d96bbe 100644 --- a/src/sp_config.c +++ b/src/sp_config.c | |||
| @@ -4,102 +4,140 @@ | |||
| 4 | 4 | ||
| 5 | #include "php_snuffleupagus.h" | 5 | #include "php_snuffleupagus.h" |
| 6 | 6 | ||
| 7 | size_t sp_line_no; | ||
| 8 | 7 | ||
| 9 | static sp_config_tokens const sp_func[] = { | 8 | static zend_result sp_process_config_root(sp_parsed_keyword *parsed_rule) { |
| 10 | {.func = parse_unserialize, .token = SP_TOKEN_UNSERIALIZE_HMAC}, | 9 | sp_config_keyword sp_func[] = { |
| 11 | {.func = parse_random, .token = SP_TOKEN_HARDEN_RANDOM}, | 10 | {parse_unserialize, SP_TOKEN_UNSERIALIZE_HMAC, SNUFFLEUPAGUS_G(config).config_unserialize}, |
| 12 | {.func = parse_log_media, .token = SP_TOKEN_LOG_MEDIA}, | 11 | {parse_enable, SP_TOKEN_HARDEN_RANDOM, &(SNUFFLEUPAGUS_G(config).config_random->enable)}, |
| 13 | {.func = parse_disabled_functions, .token = SP_TOKEN_DISABLE_FUNC}, | 12 | {parse_log_media, SP_TOKEN_LOG_MEDIA, &(SNUFFLEUPAGUS_G(config).log_media)}, |
| 14 | {.func = parse_readonly_exec, .token = SP_TOKEN_READONLY_EXEC}, | 13 | {parse_disabled_functions, SP_TOKEN_DISABLE_FUNC, NULL}, |
| 15 | {.func = parse_global_strict, .token = SP_TOKEN_GLOBAL_STRICT}, | 14 | {parse_readonly_exec, SP_TOKEN_READONLY_EXEC, SNUFFLEUPAGUS_G(config).config_readonly_exec}, |
| 16 | {.func = parse_upload_validation, .token = SP_TOKEN_UPLOAD_VALIDATION}, | 15 | {parse_enable, SP_TOKEN_GLOBAL_STRICT, &(SNUFFLEUPAGUS_G(config).config_global_strict->enable)}, |
| 17 | {.func = parse_cookie, .token = SP_TOKEN_COOKIE_ENCRYPTION}, | 16 | {parse_upload_validation, SP_TOKEN_UPLOAD_VALIDATION, SNUFFLEUPAGUS_G(config).config_upload_validation}, |
| 18 | {.func = parse_global, .token = SP_TOKEN_GLOBAL}, | 17 | {parse_cookie, SP_TOKEN_COOKIE_ENCRYPTION, NULL}, |
| 19 | {.func = parse_auto_cookie_secure, .token = SP_TOKEN_AUTO_COOKIE_SECURE}, | 18 | {parse_global, SP_TOKEN_GLOBAL, NULL}, |
| 20 | {.func = parse_disable_xxe, .token = SP_TOKEN_DISABLE_XXE}, | 19 | {parse_enable, SP_TOKEN_AUTO_COOKIE_SECURE, &(SNUFFLEUPAGUS_G(config).config_auto_cookie_secure->enable)}, |
| 21 | {.func = parse_eval_blacklist, .token = SP_TOKEN_EVAL_BLACKLIST}, | 20 | {parse_enable, SP_TOKEN_DISABLE_XXE, &(SNUFFLEUPAGUS_G(config).config_disable_xxe->enable)}, |
| 22 | {.func = parse_eval_whitelist, .token = SP_TOKEN_EVAL_WHITELIST}, | 21 | {parse_eval_filter_conf, SP_TOKEN_EVAL_BLACKLIST, &(SNUFFLEUPAGUS_G(config).config_eval->blacklist)}, |
| 23 | {.func = parse_session, .token = SP_TOKEN_SESSION_ENCRYPTION}, | 22 | {parse_eval_filter_conf, SP_TOKEN_EVAL_WHITELIST, &(SNUFFLEUPAGUS_G(config).config_eval->whitelist)}, |
| 24 | {.func = parse_sloppy_comparison, .token = SP_TOKEN_SLOPPY_COMPARISON}, | 23 | {parse_session, SP_TOKEN_SESSION_ENCRYPTION, SNUFFLEUPAGUS_G(config).config_session}, |
| 25 | {.func = parse_wrapper_whitelist, .token = SP_TOKEN_ALLOW_WRAPPERS}, | 24 | {parse_enable, SP_TOKEN_SLOPPY_COMPARISON, &(SNUFFLEUPAGUS_G(config).config_sloppy->enable)}, |
| 26 | {.func = parse_ini_protection, .token = ".ini_protection"}, | 25 | {parse_wrapper_whitelist, SP_TOKEN_ALLOW_WRAPPERS, SNUFFLEUPAGUS_G(config).config_wrapper}, |
| 27 | {.func = parse_ini_entry, .token = ".ini"}, | 26 | {parse_ini_protection, SP_TOKEN_INI_PROTECTION, SNUFFLEUPAGUS_G(config).config_ini}, |
| 28 | {NULL, NULL}}; | 27 | {parse_ini_entry, SP_TOKEN_INI, SNUFFLEUPAGUS_G(config).config_unserialize}, |
| 28 | {NULL, NULL, NULL}}; | ||
| 29 | return sp_process_rule(parsed_rule, sp_func); | ||
| 30 | } | ||
| 29 | 31 | ||
| 30 | /* Top level keyword parsing */ | 32 | zend_result sp_parse_config(const char *filename) { |
| 33 | FILE *fd = fopen(filename, "rb"); | ||
| 34 | if (fd == NULL) { | ||
| 35 | sp_log_err("config", "Could not open configuration file %s : %s", filename, strerror(errno)); | ||
| 36 | return FAILURE; | ||
| 37 | } | ||
| 31 | 38 | ||
| 32 | static int parse_line(char *line) { | 39 | size_t step = 8192; |
| 33 | char *ptr = line; | 40 | size_t max_len = step, len = 0; |
| 41 | zend_string *data = zend_string_alloc(max_len, 0); | ||
| 42 | char *ptr = ZSTR_VAL(data); | ||
| 34 | 43 | ||
| 35 | while (*ptr == ' ' || *ptr == '\t') { | 44 | size_t bytes; |
| 36 | ++ptr; | 45 | while ((bytes = fread(ptr, 1, max_len - len, fd))) { |
| 46 | len += bytes; | ||
| 47 | if (max_len - len <= 0) { | ||
| 48 | max_len += step; | ||
| 49 | data = zend_string_extend(data, max_len, 0); | ||
| 50 | ptr = ZSTR_VAL(data) + len; | ||
| 51 | } else { | ||
| 52 | ptr += bytes; | ||
| 53 | } | ||
| 37 | } | 54 | } |
| 55 | fclose(fd); | ||
| 38 | 56 | ||
| 39 | if (!*ptr || *ptr == '#' || *ptr == ';') { | 57 | data = zend_string_truncate(data, len, 0); |
| 40 | return 0; | 58 | ZSTR_VAL(data)[len] = 0; |
| 41 | } | 59 | |
| 60 | int ret = sp_config_scan(ZSTR_VAL(data), sp_process_config_root); | ||
| 61 | |||
| 62 | zend_string_release_ex(data, 0); | ||
| 63 | |||
| 64 | return ret; | ||
| 65 | } | ||
| 42 | 66 | ||
| 43 | if (strncmp(ptr, SP_TOKEN_BASE, strlen(SP_TOKEN_BASE))) { | ||
| 44 | sp_log_err("config", "Invalid configuration prefix for '%s' on line %zu", | ||
| 45 | line, sp_line_no); | ||
| 46 | return -1; | ||
| 47 | } | ||
| 48 | ptr += strlen(SP_TOKEN_BASE); | ||
| 49 | 67 | ||
| 50 | for (size_t i = 0; sp_func[i].func; i++) { | 68 | zend_result sp_process_rule(sp_parsed_keyword *parsed_rule, sp_config_keyword *config_keywords) { |
| 51 | if (!strncmp(sp_func[i].token, ptr, strlen(sp_func[i].token))) { | 69 | for (sp_parsed_keyword *kw = parsed_rule; kw->kw; kw++) { |
| 52 | return sp_func[i].func(ptr + strlen(sp_func[i].token)); | 70 | bool found_kw = false; |
| 71 | for (sp_config_keyword *ckw = config_keywords; ckw->func; ckw++) { | ||
| 72 | if (kw->kwlen == strlen(ckw->token) && !strncmp(kw->kw, ckw->token, kw->kwlen)) { | ||
| 73 | if (ckw->func) { | ||
| 74 | int ret = ckw->func(ckw->token, kw, ckw->retval); | ||
| 75 | switch (ret) { | ||
| 76 | case SP_PARSER_SUCCESS: | ||
| 77 | break; | ||
| 78 | case SP_PARSER_ERROR: | ||
| 79 | return FAILURE; | ||
| 80 | case SP_PARSER_STOP: | ||
| 81 | return SUCCESS; | ||
| 82 | } | ||
| 83 | } | ||
| 84 | found_kw = true; | ||
| 85 | break; | ||
| 86 | } | ||
| 87 | } | ||
| 88 | |||
| 89 | if (!found_kw) { | ||
| 90 | zend_string *kwname = zend_string_init(kw->kw, kw->kwlen, 0); | ||
| 91 | sp_log_err("config", "Unexpected keyword '%s' on line %d", ZSTR_VAL(kwname), kw->lineno); | ||
| 92 | zend_string_release_ex(kwname, 0); | ||
| 93 | return FAILURE; | ||
| 53 | } | 94 | } |
| 54 | } | 95 | } |
| 55 | sp_log_err("config", "Invalid configuration section '%s' on line %zu", line, | 96 | return SUCCESS; |
| 56 | sp_line_no); | ||
| 57 | return -1; | ||
| 58 | } | 97 | } |
| 59 | 98 | ||
| 60 | /* keyword parsing */ | ||
| 61 | #define CHECK_DUPLICATE_KEYWORD(retval) \ | 99 | #define CHECK_DUPLICATE_KEYWORD(retval) \ |
| 62 | if (*(void**)(retval)) { \ | 100 | if (*(void**)(retval)) { \ |
| 63 | sp_log_err("config", "duplicate %s) on line %zu near `%s`", keyword, sp_line_no, line); \ | 101 | sp_log_err("config", "duplicate keyword '%s' on line %zu", token, kw->lineno); \ |
| 64 | return -1; } | 102 | return SP_PARSER_ERROR; } |
| 65 | 103 | ||
| 66 | 104 | ||
| 67 | int parse_empty(char *restrict line, char *restrict keyword, void *retval) { | 105 | SP_PARSEKW_FN(parse_empty) { |
| 106 | if (kw->arglen) { | ||
| 107 | sp_log_err("config", "Unexpected argument for keyword '%s' - it should be '%s()' on line %zu", token, token, kw->lineno); | ||
| 108 | return SP_PARSER_ERROR; | ||
| 109 | } | ||
| 110 | if (kw->argtype != SP_ARGTYPE_EMPTY) { | ||
| 111 | sp_log_err("config", "Missing paranthesis for keyword '%s' - it should be '%s()' on line %zu", token, token, kw->lineno); | ||
| 112 | return SP_PARSER_ERROR; | ||
| 113 | } | ||
| 68 | *(bool *)retval = true; | 114 | *(bool *)retval = true; |
| 69 | return 0; | 115 | return SP_PARSER_SUCCESS; |
| 70 | } | 116 | } |
| 71 | 117 | ||
| 72 | int parse_list(char *restrict line, char *restrict keyword, void *list_ptr) { | 118 | SP_PARSEKW_FN(parse_list) { |
| 73 | CHECK_DUPLICATE_KEYWORD(list_ptr); | 119 | CHECK_DUPLICATE_KEYWORD(retval); |
| 74 | zend_string *value = NULL; | ||
| 75 | sp_list_node **list = list_ptr; | ||
| 76 | char *token, *tmp; | ||
| 77 | 120 | ||
| 78 | size_t consumed = 0; | 121 | sp_list_node **list = retval; |
| 79 | value = get_param(&consumed, line, SP_TYPE_STR, keyword); | 122 | char *tok, *tmp; |
| 80 | if (!value) { | 123 | |
| 81 | return -1; | 124 | SP_PARSE_ARG(value); |
| 82 | } | ||
| 83 | 125 | ||
| 84 | tmp = ZSTR_VAL(value); | 126 | tmp = ZSTR_VAL(value); |
| 85 | while (1) { | 127 | while (1) { |
| 86 | token = strsep(&tmp, ","); | 128 | tok = strsep(&tmp, ","); |
| 87 | if (token == NULL) { | 129 | if (tok == NULL) { |
| 88 | break; | 130 | break; |
| 89 | } | 131 | } |
| 90 | *list = sp_list_insert(*list, zend_string_init(token, strlen(token), 1)); | 132 | *list = sp_list_insert(*list, zend_string_init(tok, strlen(tok), 1)); |
| 91 | } | 133 | } |
| 134 | zend_string_release(value); | ||
| 92 | 135 | ||
| 93 | pefree(value, 1); | 136 | return SP_PARSER_SUCCESS; |
| 94 | return consumed; | ||
| 95 | } | 137 | } |
| 96 | 138 | ||
| 97 | int parse_php_type(char *restrict line, char *restrict keyword, void *retval) { | 139 | SP_PARSEKW_FN(parse_php_type) { |
| 98 | size_t consumed = 0; | 140 | SP_PARSE_ARG(value); |
| 99 | zend_string *value = get_param(&consumed, line, SP_TYPE_STR, keyword); | ||
| 100 | if (!value) { | ||
| 101 | return -1; | ||
| 102 | } | ||
| 103 | 141 | ||
| 104 | if (zend_string_equals_literal_ci(value, "undef")) { | 142 | if (zend_string_equals_literal_ci(value, "undef")) { |
| 105 | *(sp_php_type *)retval = SP_PHP_TYPE_UNDEF; | 143 | *(sp_php_type *)retval = SP_PHP_TYPE_UNDEF; |
| @@ -124,113 +162,88 @@ int parse_php_type(char *restrict line, char *restrict keyword, void *retval) { | |||
| 124 | } else if (zend_string_equals_literal_ci(value, "reference")) { | 162 | } else if (zend_string_equals_literal_ci(value, "reference")) { |
| 125 | *(sp_php_type *)retval = SP_PHP_TYPE_REFERENCE; | 163 | *(sp_php_type *)retval = SP_PHP_TYPE_REFERENCE; |
| 126 | } else { | 164 | } else { |
| 127 | pefree(value, 1); | 165 | zend_string_release(value); |
| 128 | sp_log_err("error", | 166 | sp_log_err("error", ".%s() is expecting a valid php type ('false', 'true'," |
| 129 | "%s) is expecting a valid php type ('false', 'true'," | ||
| 130 | " 'array'. 'object', 'long', 'double', 'null', 'resource', " | 167 | " 'array'. 'object', 'long', 'double', 'null', 'resource', " |
| 131 | "'reference', 'undef') on line %zu", | 168 | "'reference', 'undef') on line %zu", token, kw->lineno); |
| 132 | keyword, sp_line_no); | 169 | return SP_PARSER_ERROR; |
| 133 | return -1; | ||
| 134 | } | 170 | } |
| 135 | pefree(value, 1); | 171 | zend_string_release(value); |
| 136 | return consumed; | 172 | return SP_PARSER_SUCCESS; |
| 137 | } | 173 | } |
| 138 | 174 | ||
| 139 | int parse_str(char *restrict line, char *restrict keyword, void *retval) { | 175 | |
| 176 | SP_PARSEKW_FN(parse_str) { | ||
| 140 | CHECK_DUPLICATE_KEYWORD(retval); | 177 | CHECK_DUPLICATE_KEYWORD(retval); |
| 141 | zend_string *value = NULL; | 178 | SP_PARSE_ARG(value); |
| 142 | 179 | ||
| 143 | size_t consumed = 0; | 180 | *(zend_string **)retval = value; |
| 144 | value = get_param(&consumed, line, SP_TYPE_STR, keyword); | 181 | |
| 145 | if (value) { | 182 | return SP_PARSER_SUCCESS; |
| 146 | *(zend_string **)retval = value; | ||
| 147 | return consumed; | ||
| 148 | } | ||
| 149 | return -1; | ||
| 150 | } | 183 | } |
| 151 | 184 | ||
| 152 | int parse_cidr(char *restrict line, char *restrict keyword, void *retval) { | 185 | SP_PARSEKW_FN(parse_int) { |
| 153 | CHECK_DUPLICATE_KEYWORD(retval); | 186 | int ret = SP_PARSER_SUCCESS; |
| 187 | SP_PARSE_ARG(value); | ||
| 188 | |||
| 189 | char *endptr; | ||
| 190 | errno = 0; | ||
| 191 | *(int*)retval = (int)strtoimax(ZSTR_VAL(value), &endptr, 10); | ||
| 192 | if (errno != 0 || !endptr || endptr == ZSTR_VAL(value)) { | ||
| 193 | sp_log_err("config", "Failed to parse arg '%s' of `%s` on line %zu", ZSTR_VAL(value), token, kw->lineno); | ||
| 194 | ret = SP_PARSER_ERROR; | ||
| 195 | } | ||
| 196 | zend_string_release(value); | ||
| 197 | return ret; | ||
| 198 | } | ||
| 154 | 199 | ||
| 155 | size_t consumed = 0; | 200 | SP_PARSEKW_FN(parse_ulong) { |
| 156 | zend_string *value = get_param(&consumed, line, SP_TYPE_STR, keyword); | 201 | int ret = SP_PARSER_SUCCESS; |
| 202 | SP_PARSE_ARG(value); | ||
| 157 | 203 | ||
| 158 | if (!value) { | 204 | char *endptr; |
| 159 | sp_log_err("config", "%s doesn't contain a valid cidr on line %zu", line, sp_line_no); | 205 | errno = 0; |
| 160 | return -1; | 206 | *(u_long*)retval = (u_long)strtoul(ZSTR_VAL(value), &endptr, 10); |
| 207 | if (errno != 0 || !endptr || endptr == ZSTR_VAL(value)) { | ||
| 208 | sp_log_err("config", "Failed to parse arg '%s' of `%s` on line %zu", ZSTR_VAL(value), token, kw->lineno); | ||
| 209 | ret = SP_PARSER_ERROR; | ||
| 161 | } | 210 | } |
| 211 | zend_string_release(value); | ||
| 212 | return ret; | ||
| 213 | } | ||
| 214 | |||
| 215 | SP_PARSEKW_FN(parse_cidr) { | ||
| 216 | CHECK_DUPLICATE_KEYWORD(retval); | ||
| 217 | SP_PARSE_ARG(value); | ||
| 162 | 218 | ||
| 163 | sp_cidr *cidr = pecalloc(sizeof(sp_cidr), 1, 1); | 219 | sp_cidr *cidr = pecalloc(sizeof(sp_cidr), 1, 1); |
| 164 | 220 | ||
| 165 | if (0 != get_ip_and_cidr(ZSTR_VAL(value), cidr)) { | 221 | if (0 != get_ip_and_cidr(ZSTR_VAL(value), cidr)) { |
| 166 | pefree(cidr, 1); | 222 | pefree(cidr, 1); |
| 167 | *(sp_cidr **)retval = NULL; | 223 | cidr = NULL; |
| 168 | return -1; | ||
| 169 | } | 224 | } |
| 170 | 225 | ||
| 171 | *(sp_cidr **)retval = cidr; | 226 | *(sp_cidr **)retval = cidr; |
| 172 | return consumed; | 227 | return cidr ? SP_PARSER_SUCCESS : SP_PARSER_ERROR; |
| 173 | } | 228 | } |
| 174 | 229 | ||
| 175 | int parse_regexp(char *restrict line, char *restrict keyword, void *retval) { | 230 | SP_PARSEKW_FN(parse_regexp) { |
| 176 | /* TODO: Do we want to use pcre_study? | 231 | /* TODO: Do we want to use pcre_study? |
| 177 | * (http://www.pcre.org/original/doc/html/pcre_study.html) | 232 | * (http://www.pcre.org/original/doc/html/pcre_study.html) |
| 178 | * maybe not: http://sljit.sourceforge.net/pcre.html*/ | 233 | * maybe not: http://sljit.sourceforge.net/pcre.html*/ |
| 179 | CHECK_DUPLICATE_KEYWORD(retval); | 234 | CHECK_DUPLICATE_KEYWORD(retval); |
| 235 | SP_PARSE_ARG(value); | ||
| 180 | 236 | ||
| 181 | size_t consumed = 0; | 237 | sp_pcre *compiled_re = sp_pcre_compile(ZSTR_VAL(value)); |
| 182 | zend_string *value = get_param(&consumed, line, SP_TYPE_STR, keyword); | 238 | if (!compiled_re) { |
| 183 | 239 | sp_log_err("config", "Invalid regexp '%s' for '.%s()' on line %zu", ZSTR_VAL(value), token, kw->lineno); | |
| 184 | if (value) { | 240 | zend_string_release_ex(value, 1); |
| 185 | sp_pcre *compiled_re = sp_pcre_compile(ZSTR_VAL(value)); | 241 | return SP_PARSER_ERROR; |
| 186 | if (NULL != compiled_re) { | ||
| 187 | *(sp_pcre **)retval = compiled_re; | ||
| 188 | return consumed; | ||
| 189 | } | ||
| 190 | } | ||
| 191 | char *closing_paren = strchr(line, ')'); | ||
| 192 | if (NULL != closing_paren) { | ||
| 193 | closing_paren[0] = '\0'; | ||
| 194 | } | 242 | } |
| 195 | sp_log_err("config", | ||
| 196 | "'%s)' is expecting a valid regexp, and not '%s' on line %zu", | ||
| 197 | keyword, line, sp_line_no); | ||
| 198 | return -1; | ||
| 199 | } | ||
| 200 | 243 | ||
| 201 | int sp_parse_config(const char *conf_file) { | 244 | *(sp_pcre **)retval = compiled_re; |
| 202 | FILE *fd = fopen(conf_file, "r"); | ||
| 203 | char *lineptr = NULL; | ||
| 204 | size_t n = 0; | ||
| 205 | sp_line_no = 1; | ||
| 206 | 245 | ||
| 207 | if (fd == NULL) { | 246 | return SP_PARSER_SUCCESS; |
| 208 | sp_log_err("config", "Could not open configuration file %s : %s", conf_file, | ||
| 209 | strerror(errno)); | ||
| 210 | return FAILURE; | ||
| 211 | } | ||
| 212 | |||
| 213 | while (getline(&lineptr, &n, fd) > 0) { | ||
| 214 | /* We trash the terminal `\n`. This simplify the display of logs. */ | ||
| 215 | if (lineptr[strlen(lineptr) - 1] == '\n') { | ||
| 216 | if (strlen(lineptr) >= 2 && lineptr[strlen(lineptr) - 2] == '\r') { | ||
| 217 | lineptr[strlen(lineptr) - 2] = '\0'; | ||
| 218 | } else { | ||
| 219 | lineptr[strlen(lineptr) - 1] = '\0'; | ||
| 220 | } | ||
| 221 | } | ||
| 222 | if (parse_line(lineptr) == -1) { | ||
| 223 | fclose(fd); | ||
| 224 | free(lineptr); | ||
| 225 | return FAILURE; | ||
| 226 | } | ||
| 227 | free(lineptr); | ||
| 228 | lineptr = NULL; | ||
| 229 | n = 0; | ||
| 230 | sp_line_no++; | ||
| 231 | } | ||
| 232 | fclose(fd); | ||
| 233 | return SUCCESS; | ||
| 234 | } | 247 | } |
| 235 | 248 | ||
| 236 | void sp_free_disabled_function(void *data) { | 249 | void sp_free_disabled_function(void *data) { |
| @@ -292,4 +305,4 @@ void sp_free_ini_entry(void *data) { | |||
| 292 | sp_pcre_free(entry->regexp); | 305 | sp_pcre_free(entry->regexp); |
| 293 | sp_free_zstr(entry->msg); | 306 | sp_free_zstr(entry->msg); |
| 294 | sp_free_zstr(entry->set); | 307 | sp_free_zstr(entry->set); |
| 295 | } \ No newline at end of file | 308 | } |
