1 /*
2 +----------------------------------------------------------------------+
3 | Copyright (c) The PHP Group |
4 +----------------------------------------------------------------------+
5 | This source file is subject to version 3.01 of the PHP license, |
6 | that is bundled with this package in the file LICENSE, and is |
7 | available through the world-wide-web at the following url: |
8 | https://www.php.net/license/3_01.txt |
9 | If you did not receive a copy of the PHP license and are unable to |
10 | obtain it through the world-wide-web, please send a note to |
11 | license@php.net so we can mail you a copy immediately. |
12 +----------------------------------------------------------------------+
13 | Authors: Nikita Popov <nikic@php.net> |
14 +----------------------------------------------------------------------+
15 */
16
17 #include <main/php.h>
18
19 #if defined(__FreeBSD__)
20 # include <sys/sysctl.h>
21 #endif
22
23 #include "fuzzer.h"
24 #include "fuzzer-sapi.h"
25 #include "zend_exceptions.h"
26
27 #define FILE_NAME "/tmp/fuzzer.php"
28 #define MAX_STEPS 1000
29 #define MAX_SIZE (8 * 1024)
30 static uint32_t steps_left;
31 static bool bailed_out = false;
32
33 /* Because the fuzzer is always compiled with clang,
34 * we can assume that we don't use global registers / hybrid VM. */
35 typedef int (ZEND_FASTCALL *opcode_handler_t)(zend_execute_data *);
36
fuzzer_bailout(void)37 static zend_always_inline void fuzzer_bailout(void) {
38 bailed_out = true;
39 zend_bailout();
40 }
41
fuzzer_step(void)42 static zend_always_inline void fuzzer_step(void) {
43 if (--steps_left == 0) {
44 /* Reset steps before bailing out, so code running after bailout (e.g. in
45 * destructors) will get another MAX_STEPS, rather than UINT32_MAX steps. */
46 steps_left = MAX_STEPS;
47 fuzzer_bailout();
48 }
49 }
50
51 static void (*orig_execute_ex)(zend_execute_data *execute_data);
52
fuzzer_execute_ex(zend_execute_data * execute_data)53 static void fuzzer_execute_ex(zend_execute_data *execute_data) {
54 while (1) {
55 int ret;
56 fuzzer_step();
57 if ((ret = ((opcode_handler_t) EX(opline)->handler)(execute_data)) != 0) {
58 if (ret > 0) {
59 execute_data = EG(current_execute_data);
60 } else {
61 return;
62 }
63 }
64 }
65 }
66
67 static zend_op_array *(*orig_compile_string)(
68 zend_string *source_string, const char *filename, zend_compile_position position);
69
fuzzer_compile_string(zend_string * str,const char * filename,zend_compile_position position)70 static zend_op_array *fuzzer_compile_string(
71 zend_string *str, const char *filename, zend_compile_position position) {
72 if (ZSTR_LEN(str) > MAX_SIZE) {
73 /* Avoid compiling huge inputs via eval(). */
74 fuzzer_bailout();
75 }
76
77 return orig_compile_string(str, filename, position);
78 }
79
80 static void (*orig_execute_internal)(zend_execute_data *execute_data, zval *return_value);
81
fuzzer_execute_internal(zend_execute_data * execute_data,zval * return_value)82 static void fuzzer_execute_internal(zend_execute_data *execute_data, zval *return_value) {
83 fuzzer_step();
84
85 uint32_t num_args = ZEND_CALL_NUM_ARGS(execute_data);
86 for (uint32_t i = 0; i < num_args; i++) {
87 /* Some internal functions like preg_replace() may be slow on large inputs.
88 * Limit the maximum size of string inputs. */
89 zval *arg = ZEND_CALL_VAR_NUM(execute_data, i);
90 if (Z_TYPE_P(arg) == IS_STRING && Z_STRLEN_P(arg) > MAX_SIZE) {
91 fuzzer_bailout();
92 }
93 }
94
95 orig_execute_internal(execute_data, return_value);
96 }
97
fuzzer_init_php_for_execute(const char * extra_ini)98 static void fuzzer_init_php_for_execute(const char *extra_ini) {
99 /* Compilation will often trigger fatal errors.
100 * Use tracked allocation mode to avoid leaks in that case. */
101 putenv("USE_TRACKED_ALLOC=1");
102
103 /* Just like other SAPIs, ignore SIGPIPEs. */
104 signal(SIGPIPE, SIG_IGN);
105
106 fuzzer_init_php(extra_ini);
107
108 orig_execute_ex = zend_execute_ex;
109 zend_execute_ex = fuzzer_execute_ex;
110 orig_execute_internal = zend_execute_internal ? zend_execute_internal : execute_internal;
111 zend_execute_internal = fuzzer_execute_internal;
112 orig_compile_string = zend_compile_string;
113 zend_compile_string = fuzzer_compile_string;
114 }
115
create_file(void)116 ZEND_ATTRIBUTE_UNUSED static void create_file(void) {
117 /* For opcache_invalidate() to work, the dummy file name used for fuzzing needs to
118 * actually exist. */
119 FILE *f = fopen(FILE_NAME, "w");
120 fclose(f);
121 }
122
opcache_invalidate(void)123 ZEND_ATTRIBUTE_UNUSED static void opcache_invalidate(void) {
124 steps_left = MAX_STEPS;
125 zend_exception_save();
126 zval retval, func, args[2];
127 ZVAL_STRING(&func, "opcache_invalidate");
128 ZVAL_STRING(&args[0], FILE_NAME);
129 ZVAL_TRUE(&args[1]);
130 call_user_function(CG(function_table), NULL, &func, &retval, 2, args);
131 ZEND_ASSERT(Z_TYPE(retval) == IS_TRUE);
132 zval_ptr_dtor(&args[0]);
133 zval_ptr_dtor(&retval);
134 zval_ptr_dtor(&func);
135 zend_exception_restore();
136 }
137
get_opcache_path(void)138 ZEND_ATTRIBUTE_UNUSED char *get_opcache_path(void) {
139 /* Try relative to cwd. */
140 char *p = realpath("modules/opcache.so", NULL);
141 if (p) {
142 return p;
143 }
144
145 /* Try relative to binary location. */
146 char path[MAXPATHLEN];
147 #if defined(__FreeBSD__)
148 size_t pathlen = sizeof(path);
149 int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1};
150 if (sysctl(mib, 4, path, &pathlen, NULL, 0) < 0) {
151 #else
152 if (readlink("/proc/self/exe", path, sizeof(path)) < 0) {
153 #endif
154 ZEND_ASSERT(0 && "Failed to get binary path");
155 return NULL;
156 }
157
158 /* Get basename. */
159 char *last_sep = strrchr(path, '/');
160 if (last_sep) {
161 *last_sep = '\0';
162 }
163
164 strlcat(path, "/modules/opcache.so", sizeof(path));
165 return realpath(path, NULL);
166 }
167