1 #include "php_snuffleupagus.h"
2 
parse_enable(char * line,bool * restrict retval,bool * restrict simulation)3 static int parse_enable(char *line, bool *restrict retval,
4                         bool *restrict simulation) {
5   bool enable = false, disable = false;
6   sp_config_functions sp_config_funcs[] = {
7       {parse_empty, SP_TOKEN_ENABLE, &(enable)},
8       {parse_empty, SP_TOKEN_DISABLE, &(disable)},
9       {parse_empty, SP_TOKEN_SIMULATION, simulation},
10       {0, 0, 0}};
11 
12   int ret = parse_keywords(sp_config_funcs, line);
13 
14   if (0 != ret) {
15     return ret;
16   }
17 
18   if (!(enable ^ disable)) {
19     sp_log_err("config", "A rule can't be enabled and disabled on line %zu",
20                sp_line_no);
21     return -1;
22   }
23 
24   *retval = enable;
25 
26   return ret;
27 }
28 
parse_session(char * line)29 int parse_session(char *line) {
30   sp_config_session *session = pecalloc(sizeof(sp_config_session), 1, 0);
31 
32   sp_config_functions sp_config_funcs_session_encryption[] = {
33       {parse_empty, SP_TOKEN_ENCRYPT, &(session->encrypt)},
34       {parse_empty, SP_TOKEN_SIMULATION, &(session->simulation)},
35       {0, 0, 0}};
36   int ret = parse_keywords(sp_config_funcs_session_encryption, line);
37   if (0 != ret) {
38     return ret;
39   }
40 
41 #if (!HAVE_PHP_SESSION || defined(COMPILE_DL_SESSION))
42   sp_log_err(
43       "config",
44       "You're trying to use the session cookie encryption feature "
45       "on line %zu without having session support statically built into PHP. "
46       "This isn't supported, see "
47       "https://github.com/jvoisin/snuffleupagus/issues/278 for details.",
48       sp_line_no);
49   pefree(session, 0);
50   return -1;
51 #endif
52 
53   if (session->encrypt) {
54     if (0 == (SNUFFLEUPAGUS_G(config).config_snuffleupagus->cookies_env_var)) {
55       sp_log_err(
56           "config",
57           "You're trying to use the session cookie encryption feature "
58           "on line %zu without having set the `.cookie_env_var` option in"
59           "`sp.global`: please set it first",
60           sp_line_no);
61       pefree(session, 0);
62       return -1;
63     } else if (0 ==
64                (SNUFFLEUPAGUS_G(config).config_snuffleupagus->encryption_key)) {
65       sp_log_err("config",
66                  "You're trying to use the session cookie encryption feature "
67                  "on line %zu without having set the `.secret_key` option in"
68                  "`sp.global`: please set it first",
69                  sp_line_no);
70       pefree(session, 0);
71       return -1;
72     }
73   }
74 
75   SNUFFLEUPAGUS_G(config).config_session->encrypt = session->encrypt;
76   SNUFFLEUPAGUS_G(config).config_session->simulation = session->simulation;
77   pefree(session, 0);
78   return ret;
79 }
80 
parse_random(char * line)81 int parse_random(char *line) {
82   return parse_enable(line, &(SNUFFLEUPAGUS_G(config).config_random->enable),
83                       NULL);
84 }
85 
parse_log_media(char * line)86 int parse_log_media(char *line) {
87   size_t consumed = 0;
88   zend_string *value =
89       get_param(&consumed, line, SP_TYPE_STR, SP_TOKEN_LOG_MEDIA);
90 
91   if (value) {
92     if (!strcmp(ZSTR_VAL(value), "php")) {
93       SNUFFLEUPAGUS_G(config).log_media = SP_ZEND;
94       return 0;
95     } else if (!strcmp(ZSTR_VAL(value), "syslog")) {
96       SNUFFLEUPAGUS_G(config).log_media = SP_SYSLOG;
97       return 0;
98     }
99   }
100   sp_log_err("config", "%s) only supports 'syslog' or 'php', on line %zu",
101              SP_TOKEN_LOG_MEDIA, sp_line_no);
102   return -1;
103 }
104 
parse_sloppy_comparison(char * line)105 int parse_sloppy_comparison(char *line) {
106   return parse_enable(line, &(SNUFFLEUPAGUS_G(config).config_sloppy->enable),
107                       NULL);
108 }
109 
parse_disable_xxe(char * line)110 int parse_disable_xxe(char *line) {
111   return parse_enable(
112       line, &(SNUFFLEUPAGUS_G(config).config_disable_xxe->enable), NULL);
113 }
114 
parse_auto_cookie_secure(char * line)115 int parse_auto_cookie_secure(char *line) {
116   return parse_enable(
117       line, &(SNUFFLEUPAGUS_G(config).config_auto_cookie_secure->enable), NULL);
118 }
119 
parse_global_strict(char * line)120 int parse_global_strict(char *line) {
121   return parse_enable(
122       line, &(SNUFFLEUPAGUS_G(config).config_global_strict->enable), NULL);
123 }
124 
parse_unserialize(char * line)125 int parse_unserialize(char *line) {
126   bool enable = false, disable = false;
127   sp_config_unserialize *unserialize =
128       SNUFFLEUPAGUS_G(config).config_unserialize;
129 
130   sp_config_functions sp_config_funcs[] = {
131       {parse_empty, SP_TOKEN_ENABLE, &(enable)},
132       {parse_empty, SP_TOKEN_DISABLE, &(disable)},
133       {parse_empty, SP_TOKEN_SIMULATION, &(unserialize->simulation)},
134       {parse_str, SP_TOKEN_DUMP, &(unserialize->dump)},
135       {0, 0, 0}};
136 
137   unserialize->textual_representation = zend_string_init(line, strlen(line), 1);
138 
139   int ret = parse_keywords(sp_config_funcs, line);
140   if (0 != ret) {
141     return ret;
142   }
143 
144   if (!(enable ^ disable)) {
145     sp_log_err("config", "A rule can't be enabled and disabled on line %zu",
146                sp_line_no);
147     return -1;
148   }
149 
150   SNUFFLEUPAGUS_G(config).config_unserialize->enable = enable;
151 
152   return ret;
153 }
154 
parse_readonly_exec(char * line)155 int parse_readonly_exec(char *line) {
156   bool enable = false, disable = false;
157   sp_config_readonly_exec *readonly_exec =
158       SNUFFLEUPAGUS_G(config).config_readonly_exec;
159 
160   sp_config_functions sp_config_funcs[] = {
161       {parse_empty, SP_TOKEN_ENABLE, &(enable)},
162       {parse_empty, SP_TOKEN_DISABLE, &(disable)},
163       {parse_empty, SP_TOKEN_SIMULATION, &(readonly_exec->simulation)},
164       {parse_str, SP_TOKEN_DUMP, &(readonly_exec->dump)},
165       {0, 0, 0}};
166 
167   readonly_exec->textual_representation =
168       zend_string_init(line, strlen(line), 1);
169   int ret = parse_keywords(sp_config_funcs, line);
170 
171   if (0 != ret) {
172     return ret;
173   }
174 
175   if (!(enable ^ disable)) {
176     sp_log_err("config", "A rule can't be enabled and disabled on line %zu",
177                sp_line_no);
178     return -1;
179   }
180 
181   SNUFFLEUPAGUS_G(config).config_readonly_exec->enable = enable;
182 
183   return ret;
184 }
185 
parse_global(char * line)186 int parse_global(char *line) {
187   sp_config_functions sp_config_funcs_global[] = {
188       {parse_str, SP_TOKEN_ENCRYPTION_KEY,
189        &(SNUFFLEUPAGUS_G(config).config_snuffleupagus->encryption_key)},
190       {parse_str, SP_TOKEN_ENV_VAR,
191        &(SNUFFLEUPAGUS_G(config).config_snuffleupagus->cookies_env_var)},
192       {0, 0, 0}};
193   return parse_keywords(sp_config_funcs_global, line);
194 }
195 
parse_eval_filter_conf(char * line,sp_list_node ** list)196 static int parse_eval_filter_conf(char *line, sp_list_node **list) {
197   sp_config_eval *eval = SNUFFLEUPAGUS_G(config).config_eval;
198 
199   sp_config_functions sp_config_funcs[] = {
200       {parse_list, SP_TOKEN_LIST, list},
201       {parse_empty, SP_TOKEN_SIMULATION,
202        &(SNUFFLEUPAGUS_G(config).config_eval->simulation)},
203       {parse_str, SP_TOKEN_DUMP, &(SNUFFLEUPAGUS_G(config).config_eval->dump)},
204       {0, 0, 0}};
205 
206   eval->textual_representation = zend_string_init(line, strlen(line), 1);
207 
208   int ret = parse_keywords(sp_config_funcs, line);
209   if (0 != ret) {
210     return ret;
211   }
212 
213   return SUCCESS;
214 }
215 
parse_wrapper_whitelist(char * line)216 int parse_wrapper_whitelist(char *line) {
217   SNUFFLEUPAGUS_G(config).config_wrapper->enabled = true;
218   sp_config_functions sp_config_funcs[] = {
219       {parse_list, SP_TOKEN_LIST,
220        &SNUFFLEUPAGUS_G(config).config_wrapper->whitelist},
221       {0, 0, 0}};
222   int ret = parse_keywords(sp_config_funcs, line);
223   if (0 != ret) {
224     return ret;
225   }
226   return SUCCESS;
227 }
228 
parse_eval_blacklist(char * line)229 int parse_eval_blacklist(char *line) {
230   return parse_eval_filter_conf(
231       line, &SNUFFLEUPAGUS_G(config).config_eval->blacklist);
232 }
233 
parse_eval_whitelist(char * line)234 int parse_eval_whitelist(char *line) {
235   return parse_eval_filter_conf(
236       line, &SNUFFLEUPAGUS_G(config).config_eval->whitelist);
237 }
238 
parse_cookie(char * line)239 int parse_cookie(char *line) {
240   int ret = 0;
241   zend_string *samesite = NULL;
242   sp_cookie *cookie = pecalloc(sizeof(sp_cookie), 1, 1);
243 
244   sp_config_functions sp_config_funcs_cookie_encryption[] = {
245       {parse_str, SP_TOKEN_NAME, &(cookie->name)},
246       {parse_regexp, SP_TOKEN_NAME_REGEXP, &(cookie->name_r)},
247       {parse_str, SP_TOKEN_SAMESITE, &samesite},
248       {parse_empty, SP_TOKEN_ENCRYPT, &cookie->encrypt},
249       {parse_empty, SP_TOKEN_SIMULATION, &cookie->simulation},
250       {0, 0, 0}};
251 
252   ret = parse_keywords(sp_config_funcs_cookie_encryption, line);
253   if (0 != ret) {
254     return ret;
255   }
256 
257   if (cookie->encrypt) {
258     if (0 == (SNUFFLEUPAGUS_G(config).config_snuffleupagus->cookies_env_var)) {
259       sp_log_err(
260           "config",
261           "You're trying to use the cookie encryption feature"
262           "on line %zu without having set the `.cookie_env_var` option in"
263           "`sp.global`: please set it first",
264           sp_line_no);
265       return -1;
266     } else if (0 ==
267                (SNUFFLEUPAGUS_G(config).config_snuffleupagus->encryption_key)) {
268       sp_log_err(
269           "config",
270           "You're trying to use the cookie encryption feature"
271           "on line %zu without having set the `.encryption_key` option in"
272           "`sp.global`: please set it first",
273           sp_line_no);
274       return -1;
275     }
276   } else if (!samesite) {
277     sp_log_err("config",
278                "You must specify a at least one action to a cookie on line "
279                "%zu",
280                sp_line_no);
281     return -1;
282   }
283   if ((!cookie->name || 0 == ZSTR_LEN(cookie->name)) && !cookie->name_r) {
284     sp_log_err("config",
285                "You must specify a cookie name/regexp on line "
286                "%zu",
287                sp_line_no);
288     return -1;
289   }
290   if (cookie->name && cookie->name_r) {
291     sp_log_err("config",
292                "name and name_r are mutually exclusive on line "
293                "%zu",
294                sp_line_no);
295     return -1;
296   }
297   if (samesite) {
298     if (zend_string_equals_literal_ci(samesite, SP_TOKEN_SAMESITE_LAX)) {
299       cookie->samesite = lax;
300     } else if (zend_string_equals_literal_ci(samesite,
301                                              SP_TOKEN_SAMESITE_STRICT)) {
302       cookie->samesite = strict;
303     } else {
304       sp_log_err(
305           "config",
306           "%s is an invalid value to samesite (expected %s or %s) on line "
307           "%zu",
308           ZSTR_VAL(samesite), SP_TOKEN_SAMESITE_LAX, SP_TOKEN_SAMESITE_STRICT,
309           sp_line_no);
310       return -1;
311     }
312   }
313   SNUFFLEUPAGUS_G(config).config_cookie->cookies =
314       sp_list_insert(SNUFFLEUPAGUS_G(config).config_cookie->cookies, cookie);
315   return SUCCESS;
316 }
317 
add_df_to_hashtable(HashTable * ht,sp_disabled_function * df)318 int add_df_to_hashtable(HashTable *ht, sp_disabled_function *df) {
319   zval *list = zend_hash_find(ht, df->function);
320 
321   if (NULL == list) {
322     zend_hash_add_ptr(ht, df->function, sp_list_insert(NULL, df));
323   } else {
324     Z_PTR_P(list) = sp_list_insert(Z_PTR_P(list), df);
325   }
326   return SUCCESS;
327 }
328 
parse_disabled_functions(char * line)329 int parse_disabled_functions(char *line) {
330   int ret = 0;
331   bool enable = true, disable = false, allow = false, drop = false;
332   zend_string *pos = NULL, *var = NULL, *param = NULL;
333   zend_string *line_number = NULL;
334   sp_disabled_function *df = pecalloc(sizeof(*df), 1, 1);
335   df->pos = -1;
336 
337   sp_config_functions sp_config_funcs_disabled_functions[] = {
338       {parse_empty, SP_TOKEN_ENABLE, &(enable)},
339       {parse_empty, SP_TOKEN_DISABLE, &(disable)},
340       {parse_str, SP_TOKEN_ALIAS, &(df->alias)},
341       {parse_empty, SP_TOKEN_SIMULATION, &(df->simulation)},
342       {parse_str, SP_TOKEN_FILENAME, &(df->filename)},
343       {parse_regexp, SP_TOKEN_FILENAME_REGEXP, &(df->r_filename)},
344       {parse_str, SP_TOKEN_FUNCTION, &(df->function)},
345       {parse_regexp, SP_TOKEN_FUNCTION_REGEXP, &(df->r_function)},
346       {parse_str, SP_TOKEN_DUMP, &(df->dump)},
347       {parse_empty, SP_TOKEN_ALLOW, &(allow)},
348       {parse_empty, SP_TOKEN_DROP, &(drop)},
349       {parse_str, SP_TOKEN_HASH, &(df->hash)},
350       {parse_str, SP_TOKEN_PARAM, &(param)},
351       {parse_regexp, SP_TOKEN_VALUE_REGEXP, &(df->r_value)},
352       {parse_str, SP_TOKEN_VALUE, &(df->value)},
353       {parse_str, SP_TOKEN_KEY, &(df->key)},
354       {parse_regexp, SP_TOKEN_KEY_REGEXP, &(df->r_key)},
355       {parse_regexp, SP_TOKEN_PARAM_REGEXP, &(df->r_param)},
356       {parse_php_type, SP_TOKEN_PARAM_TYPE, &(df->param_type)},
357       {parse_str, SP_TOKEN_RET, &(df->ret)},
358       {parse_cidr, SP_TOKEN_CIDR, &(df->cidr)},
359       {parse_regexp, SP_TOKEN_RET_REGEXP, &(df->r_ret)},
360       {parse_php_type, SP_TOKEN_RET_TYPE, &(df->ret_type)},
361       {parse_str, SP_TOKEN_LOCAL_VAR, &(var)},
362       {parse_str, SP_TOKEN_VALUE_ARG_POS, &(pos)},
363       {parse_str, SP_TOKEN_LINE_NUMBER, &(line_number)},
364       {0, 0, 0}};
365 
366   ret = parse_keywords(sp_config_funcs_disabled_functions, line);
367 
368   if (0 != ret) {
369     return ret;
370   }
371 
372 #define MUTUALLY_EXCLUSIVE(X, Y, STR1, STR2)                             \
373   if (X && Y) {                                                          \
374     sp_log_err("config",                                                 \
375                "Invalid configuration line: 'sp.disabled_functions%s': " \
376                "'.%s' and '.%s' are mutually exclusive on line %zu",     \
377                line, STR1, STR2, sp_line_no);                            \
378     return -1;                                                           \
379   }
380 
381   MUTUALLY_EXCLUSIVE(df->r_value, df->value, "r_value", "value");
382   MUTUALLY_EXCLUSIVE(df->r_function, df->function, "r_function", "function");
383   MUTUALLY_EXCLUSIVE(df->r_filename, df->filename, "r_filename", "filename");
384   MUTUALLY_EXCLUSIVE(df->r_ret, df->ret, "r_ret", "ret");
385   MUTUALLY_EXCLUSIVE(df->r_key, df->key, "r_key", "key");
386 #undef MUTUALLY_EXCLUSIVE
387 
388   if (1 <
389       ((df->r_param ? 1 : 0) + (param ? 1 : 0) + ((-1 != df->pos) ? 1 : 0))) {
390     sp_log_err(
391         "config",
392         "Invalid configuration line: 'sp.disabled_functions%s':"
393         "'.r_param', '.param' and '.pos' are mutually exclusive on line %zu",
394         line, sp_line_no);
395     return -1;
396   } else if ((df->r_key || df->key) && (df->r_value || df->value)) {
397     sp_log_err("config",
398                "Invalid configuration line: 'sp.disabled_functions%s':"
399                "`key` and `value` are mutually exclusive on line %zu",
400                line, sp_line_no);
401     return -1;
402   } else if ((df->r_ret || df->ret || df->ret_type) && (df->r_param || param)) {
403     sp_log_err("config",
404                "Invalid configuration line: 'sp.disabled_functions%s':"
405                "`ret` and `param` are mutually exclusive on line %zu",
406                line, sp_line_no);
407     return -1;
408   } else if ((df->r_ret || df->ret || df->ret_type) && (var)) {
409     sp_log_err("config",
410                "Invalid configuration line: 'sp.disabled_functions%s':"
411                "`ret` and `var` are mutually exclusive on line %zu",
412                line, sp_line_no);
413     return -1;
414   } else if ((df->r_ret || df->ret || df->ret_type) &&
415              (df->value || df->r_value)) {
416     sp_log_err("config",
417                "Invalid configuration line: 'sp.disabled_functions%s':"
418                "`ret` and `value` are mutually exclusive on line %zu",
419                line, sp_line_no);
420     return -1;
421   } else if (!(df->r_function || df->function)) {
422     sp_log_err("config",
423                "Invalid configuration line: 'sp.disabled_functions%s':"
424                " must take a function name on line %zu",
425                line, sp_line_no);
426     return -1;
427   } else if (df->filename && (*ZSTR_VAL(df->filename) != '/') &&
428              (0 !=
429               strncmp(ZSTR_VAL(df->filename), "phar://", strlen("phar://")))) {
430     sp_log_err(
431         "config",
432         "Invalid configuration line: 'sp.disabled_functions%s':"
433         "'.filename' must be an absolute path or a phar archive on line %zu",
434         line, sp_line_no);
435     return -1;
436   } else if (!(allow ^ drop)) {
437     sp_log_err("config",
438                "Invalid configuration line: 'sp.disabled_functions%s': The "
439                "rule must either be a `drop` or `allow` one on line %zu",
440                line, sp_line_no);
441     return -1;
442   }
443 
444   if (pos) {
445     errno = 0;
446     char *endptr;
447     df->pos = (int)strtol(ZSTR_VAL(pos), &endptr, 10);
448     if (errno != 0 || endptr == ZSTR_VAL(pos)) {
449       sp_log_err("config", "Failed to parse arg '%s' of `pos` on line %zu",
450                  ZSTR_VAL(pos), sp_line_no);
451       return -1;
452     }
453   }
454 
455   if (line_number) {
456     errno = 0;
457     char *endptr;
458     df->line = (unsigned int)strtoul(ZSTR_VAL(line_number), &endptr, 10);
459     if (errno != 0 || endptr == ZSTR_VAL(line_number)) {
460       sp_log_err("config", "Failed to parse arg '%s' of `line` on line %zu",
461                  ZSTR_VAL(line_number), sp_line_no);
462       return -1;
463     }
464   }
465   df->allow = allow;
466   df->textual_representation = zend_string_init(line, strlen(line), 1);
467 
468   if (df->function) {
469     df->functions_list = parse_functions_list(ZSTR_VAL(df->function));
470   }
471 
472   if (param) {
473     if (ZSTR_LEN(param) > 0 && ZSTR_VAL(param)[0] != '$') {
474       /* This is an ugly hack. We're prefixing with a `$` because otherwise
475        * the parser treats this as a constant.
476        * FIXME: Remove this, and improve our (weird) parser. */
477       char *new = pecalloc(ZSTR_LEN(param) + 2, 1, 1);
478       new[0] = '$';
479       memcpy(new + 1, ZSTR_VAL(param), ZSTR_LEN(param));
480       df->param = sp_parse_var(new);
481       free(new);
482     } else {
483       df->param = sp_parse_var(ZSTR_VAL(param));
484     }
485     if (!df->param) {
486       sp_log_err("config", "Invalid value '%s' for `param` on line %zu",
487                  ZSTR_VAL(param), sp_line_no);
488       return -1;
489     }
490   }
491 
492   if (var) {
493     if (ZSTR_LEN(var)) {
494       df->var = sp_parse_var(ZSTR_VAL(var));
495       if (!df->var) {
496         sp_log_err("config", "Invalid value '%s' for `var` on line %zu",
497                    ZSTR_VAL(var), sp_line_no);
498         return -1;
499       }
500     } else {
501       sp_log_err("config", "Empty value in `var` on line %zu", sp_line_no);
502       return -1;
503     }
504   }
505 
506   if (true == disable) {
507     return ret;
508   }
509 
510   if (df->function && zend_string_equals_literal(df->function, "print")) {
511     zend_string_release(df->function);
512     df->function = zend_string_init("echo", sizeof("echo") - 1, 1);
513   }
514 
515   if (df->function && !df->functions_list) {
516     if (df->ret || df->r_ret || df->ret_type) {
517       add_df_to_hashtable(SNUFFLEUPAGUS_G(config).config_disabled_functions_ret,
518                           df);
519     } else {
520       add_df_to_hashtable(SNUFFLEUPAGUS_G(config).config_disabled_functions,
521                           df);
522     }
523   } else {
524     if (df->ret || df->r_ret || df->ret_type) {
525       SNUFFLEUPAGUS_G(config)
526           .config_disabled_functions_reg_ret->disabled_functions =
527           sp_list_insert(
528               SNUFFLEUPAGUS_G(config)
529                   .config_disabled_functions_reg_ret->disabled_functions,
530               df);
531     } else {
532       SNUFFLEUPAGUS_G(config)
533           .config_disabled_functions_reg->disabled_functions =
534           sp_list_insert(SNUFFLEUPAGUS_G(config)
535                              .config_disabled_functions_reg->disabled_functions,
536                          df);
537     }
538   }
539   return ret;
540 }
541 
parse_upload_validation(char * line)542 int parse_upload_validation(char *line) {
543   bool disable = false, enable = false;
544   sp_config_functions sp_config_funcs_upload_validation[] = {
545       {parse_str, SP_TOKEN_UPLOAD_SCRIPT,
546        &(SNUFFLEUPAGUS_G(config).config_upload_validation->script)},
547       {parse_empty, SP_TOKEN_SIMULATION,
548        &(SNUFFLEUPAGUS_G(config).config_upload_validation->simulation)},
549       {parse_empty, SP_TOKEN_ENABLE, &(enable)},
550       {parse_empty, SP_TOKEN_DISABLE, &(disable)},
551       {0, 0, 0}};
552 
553   int ret = parse_keywords(sp_config_funcs_upload_validation, line);
554 
555   if (0 != ret) {
556     return ret;
557   }
558 
559   if (!(enable ^ disable)) {
560     sp_log_err("config", "A rule can't be enabled and disabled on line %zu",
561                sp_line_no);
562     return -1;
563   }
564   SNUFFLEUPAGUS_G(config).config_upload_validation->enable = enable;
565 
566   zend_string const *script =
567       SNUFFLEUPAGUS_G(config).config_upload_validation->script;
568 
569   if (!script) {
570     sp_log_err("config",
571                "The `script` directive is mandatory in '%s' on line %zu", line,
572                sp_line_no);
573     return -1;
574   } else if (-1 == access(ZSTR_VAL(script), F_OK)) {
575     sp_log_err("config", "The `script` (%s) doesn't exist on line %zu",
576                ZSTR_VAL(script), sp_line_no);
577     return -1;
578   } else if (-1 == access(ZSTR_VAL(script), X_OK)) {
579     sp_log_err("config", "The `script` (%s) isn't executable on line %zu",
580                ZSTR_VAL(script), sp_line_no);
581     return -1;
582   }
583 
584   return ret;
585 }
586