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