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: Sammy Kaye Powers <me@sammyk.me>                            |
14    +----------------------------------------------------------------------+
15 */
16 
17 #include <stdlib.h>
18 #include <sys/stat.h>
19 #include <fcntl.h>
20 #include <math.h>
21 
22 #include "php.h"
23 #include "zend_exceptions.h"
24 #include "php_random.h"
25 
26 #ifdef PHP_WIN32
27 # include "win32/winutil.h"
28 #endif
29 #ifdef __linux__
30 # include <sys/syscall.h>
31 #endif
32 #if HAVE_SYS_PARAM_H
33 # include <sys/param.h>
34 # if (__FreeBSD__ && __FreeBSD_version > 1200000) || defined(__DragonFly__) || defined(__sun)
35 #  include <sys/random.h>
36 # endif
37 #endif
38 
39 #if __has_feature(memory_sanitizer)
40 # include <sanitizer/msan_interface.h>
41 #endif
42 
43 #ifdef ZTS
44 int random_globals_id;
45 #else
46 php_random_globals random_globals;
47 #endif
48 
random_globals_ctor(php_random_globals * random_globals_p)49 static void random_globals_ctor(php_random_globals *random_globals_p)
50 {
51 	random_globals_p->fd = -1;
52 }
53 
random_globals_dtor(php_random_globals * random_globals_p)54 static void random_globals_dtor(php_random_globals *random_globals_p)
55 {
56 	if (random_globals_p->fd > 0) {
57 		close(random_globals_p->fd);
58 		random_globals_p->fd = -1;
59 	}
60 }
61 
62 /* {{{ */
PHP_MINIT_FUNCTION(random)63 PHP_MINIT_FUNCTION(random)
64 {
65 #ifdef ZTS
66 	ts_allocate_id(&random_globals_id, sizeof(php_random_globals), (ts_allocate_ctor)random_globals_ctor, (ts_allocate_dtor)random_globals_dtor);
67 #else
68 	random_globals_ctor(&random_globals);
69 #endif
70 
71 	return SUCCESS;
72 }
73 /* }}} */
74 
75 /* {{{ */
PHP_MSHUTDOWN_FUNCTION(random)76 PHP_MSHUTDOWN_FUNCTION(random)
77 {
78 #ifndef ZTS
79 	random_globals_dtor(&random_globals);
80 #endif
81 
82 	return SUCCESS;
83 }
84 /* }}} */
85 
86 /* {{{ php_random_bytes */
php_random_bytes(void * bytes,size_t size,bool should_throw)87 PHPAPI int php_random_bytes(void *bytes, size_t size, bool should_throw)
88 {
89 #ifdef PHP_WIN32
90 	/* Defer to CryptGenRandom on Windows */
91 	if (php_win32_get_random_bytes(bytes, size) == FAILURE) {
92 		if (should_throw) {
93 			zend_throw_exception(zend_ce_exception, "Could not gather sufficient random data", 0);
94 		}
95 		return FAILURE;
96 	}
97 #elif HAVE_DECL_ARC4RANDOM_BUF && ((defined(__OpenBSD__) && OpenBSD >= 201405) || (defined(__NetBSD__) && __NetBSD_Version__ >= 700000001) || defined(__APPLE__))
98 	arc4random_buf(bytes, size);
99 #else
100 	size_t read_bytes = 0;
101 	ssize_t n;
102 #if (defined(__linux__) && defined(SYS_getrandom)) || (defined(__FreeBSD__) && __FreeBSD_version >= 1200000) || defined(__DragonFly__) || defined(__sun)
103 	/* Linux getrandom(2) syscall or FreeBSD/DragonFlyBSD getrandom(2) function*/
104 	/* Keep reading until we get enough entropy */
105 	while (read_bytes < size) {
106 		/* Below, (bytes + read_bytes)  is pointer arithmetic.
107 
108 		   bytes   read_bytes  size
109 		     |      |           |
110 		    [#######=============] (we're going to write over the = region)
111 		             \\\\\\\\\\\\\
112 		              amount_to_read
113 
114 		*/
115 		size_t amount_to_read = size - read_bytes;
116 #if defined(__linux__)
117 		n = syscall(SYS_getrandom, bytes + read_bytes, amount_to_read, 0);
118 #else
119 		n = getrandom(bytes + read_bytes, amount_to_read, 0);
120 #endif
121 
122 		if (n == -1) {
123 			if (errno == ENOSYS) {
124 				/* This can happen if PHP was compiled against a newer kernel where getrandom()
125 				 * is available, but then runs on an older kernel without getrandom(). If this
126 				 * happens we simply fall back to reading from /dev/urandom. */
127 				ZEND_ASSERT(read_bytes == 0);
128 				break;
129 			} else if (errno == EINTR || errno == EAGAIN) {
130 				/* Try again */
131 				continue;
132 			} else {
133 			    /* If the syscall fails, fall back to reading from /dev/urandom */
134 				break;
135 			}
136 		}
137 
138 #if __has_feature(memory_sanitizer)
139 		/* MSan does not instrument manual syscall invocations. */
140 		__msan_unpoison(bytes + read_bytes, n);
141 #endif
142 		read_bytes += (size_t) n;
143 	}
144 #endif
145 	if (read_bytes < size) {
146 		int    fd = RANDOM_G(fd);
147 		struct stat st;
148 
149 		if (fd < 0) {
150 #if HAVE_DEV_URANDOM
151 			fd = open("/dev/urandom", O_RDONLY);
152 #endif
153 			if (fd < 0) {
154 				if (should_throw) {
155 					zend_throw_exception(zend_ce_exception, "Cannot open source device", 0);
156 				}
157 				return FAILURE;
158 			}
159 			/* Does the file exist and is it a character device? */
160 			if (fstat(fd, &st) != 0 ||
161 # ifdef S_ISNAM
162 					!(S_ISNAM(st.st_mode) || S_ISCHR(st.st_mode))
163 # else
164 					!S_ISCHR(st.st_mode)
165 # endif
166 			) {
167 				close(fd);
168 				if (should_throw) {
169 					zend_throw_exception(zend_ce_exception, "Error reading from source device", 0);
170 				}
171 				return FAILURE;
172 			}
173 			RANDOM_G(fd) = fd;
174 		}
175 
176 		for (read_bytes = 0; read_bytes < size; read_bytes += (size_t) n) {
177 			n = read(fd, bytes + read_bytes, size - read_bytes);
178 			if (n <= 0) {
179 				break;
180 			}
181 		}
182 
183 		if (read_bytes < size) {
184 			if (should_throw) {
185 				zend_throw_exception(zend_ce_exception, "Could not gather sufficient random data", 0);
186 			}
187 			return FAILURE;
188 		}
189 	}
190 #endif
191 
192 	return SUCCESS;
193 }
194 /* }}} */
195 
196 /* {{{ Return an arbitrary length of pseudo-random bytes as binary string */
PHP_FUNCTION(random_bytes)197 PHP_FUNCTION(random_bytes)
198 {
199 	zend_long size;
200 	zend_string *bytes;
201 
202 	ZEND_PARSE_PARAMETERS_START(1, 1)
203 		Z_PARAM_LONG(size)
204 	ZEND_PARSE_PARAMETERS_END();
205 
206 	if (size < 1) {
207 		zend_argument_value_error(1, "must be greater than 0");
208 		RETURN_THROWS();
209 	}
210 
211 	bytes = zend_string_alloc(size, 0);
212 
213 	if (php_random_bytes_throw(ZSTR_VAL(bytes), size) == FAILURE) {
214 		zend_string_release_ex(bytes, 0);
215 		RETURN_THROWS();
216 	}
217 
218 	ZSTR_VAL(bytes)[size] = '\0';
219 
220 	RETURN_STR(bytes);
221 }
222 /* }}} */
223 
224 /* {{{ */
php_random_int(zend_long min,zend_long max,zend_long * result,bool should_throw)225 PHPAPI int php_random_int(zend_long min, zend_long max, zend_long *result, bool should_throw)
226 {
227 	zend_ulong umax;
228 	zend_ulong trial;
229 
230 	if (min == max) {
231 		*result = min;
232 		return SUCCESS;
233 	}
234 
235 	umax = (zend_ulong) max - (zend_ulong) min;
236 
237 	if (php_random_bytes(&trial, sizeof(trial), should_throw) == FAILURE) {
238 		return FAILURE;
239 	}
240 
241 	/* Special case where no modulus is required */
242 	if (umax == ZEND_ULONG_MAX) {
243 		*result = (zend_long)trial;
244 		return SUCCESS;
245 	}
246 
247 	/* Increment the max so the range is inclusive of max */
248 	umax++;
249 
250 	/* Powers of two are not biased */
251 	if ((umax & (umax - 1)) != 0) {
252 		/* Ceiling under which ZEND_LONG_MAX % max == 0 */
253 		zend_ulong limit = ZEND_ULONG_MAX - (ZEND_ULONG_MAX % umax) - 1;
254 
255 		/* Discard numbers over the limit to avoid modulo bias */
256 		while (trial > limit) {
257 			if (php_random_bytes(&trial, sizeof(trial), should_throw) == FAILURE) {
258 				return FAILURE;
259 			}
260 		}
261 	}
262 
263 	*result = (zend_long)((trial % umax) + min);
264 	return SUCCESS;
265 }
266 /* }}} */
267 
268 /* {{{ Return an arbitrary pseudo-random integer */
PHP_FUNCTION(random_int)269 PHP_FUNCTION(random_int)
270 {
271 	zend_long min;
272 	zend_long max;
273 	zend_long result;
274 
275 	ZEND_PARSE_PARAMETERS_START(2, 2)
276 		Z_PARAM_LONG(min)
277 		Z_PARAM_LONG(max)
278 	ZEND_PARSE_PARAMETERS_END();
279 
280 	if (min > max) {
281 		zend_argument_value_error(1, "must be less than or equal to argument #2 ($max)");
282 		RETURN_THROWS();
283 	}
284 
285 	if (php_random_int_throw(min, max, &result) == FAILURE) {
286 		RETURN_THROWS();
287 	}
288 
289 	RETURN_LONG(result);
290 }
291 /* }}} */
292