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