Skip to content

Commit 5f1b83e

Browse files
ltnikic
authored andcommitted
Improve CSPRNG implementation
1 parent bc54d13 commit 5f1b83e

File tree

5 files changed

+140
-60
lines changed

5 files changed

+140
-60
lines changed

Zend/Zend.m4

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -401,13 +401,14 @@ if test -r "/dev/urandom" && test -c "/dev/urandom"; then
401401
AC_MSG_RESULT(yes)
402402
else
403403
AC_MSG_RESULT(no)
404-
AC_MSG_CHECKING(whether /dev/arandom exists)
405-
if test -r "/dev/arandom" && test -c "/dev/arandom"; then
406-
AC_DEFINE([HAVE_DEV_ARANDOM], 1, [Define if the target system has /dev/arandom device])
407-
AC_MSG_RESULT(yes)
408-
else
409-
AC_MSG_RESULT(no)
410-
fi
404+
fi
405+
406+
AC_MSG_CHECKING(whether /dev/arandom exists)
407+
if test -r "/dev/arandom" && test -c "/dev/arandom"; then
408+
AC_DEFINE([HAVE_DEV_ARANDOM], 1, [Define if the target system has /dev/arandom device])
409+
AC_MSG_RESULT(yes)
410+
else
411+
AC_MSG_RESULT(no)
411412
fi
412413

413414
AC_ARG_ENABLE(gcc-global-regs,

ext/standard/basic_functions.c

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1907,10 +1907,11 @@ ZEND_END_ARG_INFO()
19071907
/* }}} */
19081908
/* {{{ random.c */
19091909
ZEND_BEGIN_ARG_INFO_EX(arginfo_random_bytes, 0, 0, 0)
1910-
ZEND_ARG_INFO(0, bytes)
1910+
ZEND_ARG_INFO(0, length)
19111911
ZEND_END_ARG_INFO()
19121912

19131913
ZEND_BEGIN_ARG_INFO_EX(arginfo_random_int, 0, 0, 0)
1914+
ZEND_ARG_INFO(0, min)
19141915
ZEND_ARG_INFO(0, max)
19151916
ZEND_END_ARG_INFO()
19161917
/* }}} */
@@ -3670,6 +3671,8 @@ PHP_MINIT_FUNCTION(basic) /* {{{ */
36703671
# endif
36713672
#endif
36723673

3674+
BASIC_MINIT_SUBMODULE(random)
3675+
36733676
return SUCCESS;
36743677
}
36753678
/* }}} */
@@ -3708,6 +3711,8 @@ PHP_MSHUTDOWN_FUNCTION(basic) /* {{{ */
37083711
BASIC_MSHUTDOWN_SUBMODULE(crypt)
37093712
#endif
37103713

3714+
BASIC_MSHUTDOWN_SUBMODULE(random)
3715+
37113716
zend_hash_destroy(&basic_submodules);
37123717
return SUCCESS;
37133718
}

ext/standard/config.m4

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,11 @@ dnl Check for atomic operation API availability in Solaris
592592
dnl
593593
AC_CHECK_HEADERS([atomic.h])
594594

595+
dnl
596+
dnl Check for arc4random on BSD systems
597+
dnl
598+
AC_CHECK_DECLS([arc4random_buf])
599+
595600
dnl
596601
dnl Setup extension sources
597602
dnl

ext/standard/php_random.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,22 @@
2323

2424
PHP_FUNCTION(random_bytes);
2525
PHP_FUNCTION(random_int);
26+
27+
PHP_MINIT_FUNCTION(random);
28+
PHP_MSHUTDOWN_FUNCTION(random);
29+
30+
typedef struct {
31+
int fd;
32+
} php_random_globals;
33+
34+
#ifdef ZTS
35+
# define RANDOM_G(v) ZEND_TSRMG(random_globals_id, php_random_globals *, v)
36+
extern PHPAPI int random_globals_id;
37+
#else
38+
# define RANDOM_G(v) random_globals.v
39+
extern PHPAPI php_random_globals random_globals;
40+
#endif
41+
2642
#endif
2743

2844
/*

ext/standard/random.c

Lines changed: 105 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -24,63 +24,105 @@
2424
#include <math.h>
2525

2626
#include "php.h"
27+
#include "php_random.h"
2728

2829
#if PHP_WIN32
2930
# include "win32/winutil.h"
3031
#endif
3132

32-
// Big thanks to @ircmaxell for the help on this bit
33-
union rand_long_buffer {
34-
char buffer[8];
35-
long number;
36-
};
33+
#ifdef ZTS
34+
int random_globals_id;
35+
#else
36+
php_random_globals random_globals;
37+
#endif
38+
39+
static void random_globals_ctor(php_random_globals *random_globals_p)
40+
{
41+
random_globals_p->fd = -1;
42+
}
43+
44+
static void random_globals_dtor(php_random_globals *random_globals_p)
45+
{
46+
if (random_globals_p->fd > 0) {
47+
close(random_globals_p->fd);
48+
random_globals_p->fd = -1;
49+
}
50+
}
51+
52+
/* {{{ */
53+
PHP_MINIT_FUNCTION(random)
54+
{
55+
#ifdef ZTS
56+
ts_allocate_id(&random_globals_id, sizeof(php_random_globals), (ts_allocate_ctor)random_globals_ctor, (ts_allocate_dtor)random_globals_dtor);
57+
#else
58+
random_globals_ctor(&random_globals);
59+
#endif
60+
61+
return SUCCESS;
62+
}
63+
/* }}} */
3764

38-
// Copy/pasted from mcrypt.c
39-
static int php_random_bytes(char *bytes, zend_long size)
65+
/* {{{ */
66+
PHP_MSHUTDOWN_FUNCTION(random)
4067
{
41-
int n = 0;
68+
#ifndef ZTS
69+
random_globals_dtor(&random_globals);
70+
#endif
71+
}
72+
/* }}} */
4273

74+
/* {{{ */
75+
static int php_random_bytes(void *bytes, size_t size)
76+
{
4377
#if PHP_WIN32
44-
/* random/urandom equivalent on Windows */
45-
BYTE *win_bytes = (BYTE *) bytes;
46-
if (php_win32_get_random_bytes(win_bytes, (size_t) size) == FAILURE) {
78+
/* Defer to CryptGenRandom on Windows */
79+
if (php_win32_get_random_bytes(bytes, size) == FAILURE) {
4780
php_error_docref(NULL, E_WARNING, "Could not gather sufficient random data");
4881
return FAILURE;
4982
}
50-
n = (int)size;
5183
#else
52-
// @todo Need to cache the fd for random_int() call within loop
53-
int fd;
84+
#if HAVE_DECL_ARC4RANDOM_BUF
85+
arc4random_buf(bytes, size);
86+
#else
87+
int fd = RANDOM_G(fd);
5488
size_t read_bytes = 0;
5589

56-
fd = open("/dev/urandom", O_RDONLY);
5790
if (fd < 0) {
58-
php_error_docref(NULL, E_WARNING, "Cannot open source device");
59-
return FAILURE;
91+
#if HAVE_DEV_ARANDOM
92+
fd = open("/dev/arandom", O_RDONLY);
93+
#else
94+
#if HAVE_DEV_URANDOM
95+
fd = open("/dev/urandom", O_RDONLY);
96+
#endif // URANDOM
97+
#endif // ARANDOM
98+
if (fd < 0) {
99+
php_error_docref(NULL, E_WARNING, "Cannot open source device");
100+
return FAILURE;
101+
}
102+
103+
RANDOM_G(fd) = fd;
60104
}
105+
61106
while (read_bytes < size) {
62-
n = read(fd, bytes + read_bytes, size - read_bytes);
107+
ssize_t n = read(fd, bytes + read_bytes, size - read_bytes);
63108
if (n < 0) {
64109
break;
65110
}
66111
read_bytes += n;
67112
}
68-
n = read_bytes;
69113

70-
close(fd);
71-
if (n < size) {
114+
if (read_bytes < size) {
72115
php_error_docref(NULL, E_WARNING, "Could not gather sufficient random data");
73116
return FAILURE;
74117
}
75-
#endif
76-
77-
// @todo - Do we need to do this?
78-
bytes[size] = '\0';
118+
#endif // !ARC4RANDOM_BUF
119+
#endif // !WIN32
79120

80121
return SUCCESS;
81122
}
123+
/* }}} */
82124

83-
/* {{{ proto string random_bytes(int bytes)
125+
/* {{{ proto string random_bytes(int length)
84126
Return an arbitrary length of pseudo-random bytes as binary string */
85127
PHP_FUNCTION(random_bytes)
86128
{
@@ -91,60 +133,71 @@ PHP_FUNCTION(random_bytes)
91133
return;
92134
}
93135

94-
if (size <= 0 || size >= INT_MAX) {
95-
php_error_docref(NULL, E_WARNING, "Cannot genrate a random string with a size of less than 1 or greater than %d", INT_MAX);
136+
if (size < 1) {
137+
php_error_docref(NULL, E_WARNING, "Length must be greater than 0");
96138
RETURN_FALSE;
97139
}
98140

99141
bytes = zend_string_alloc(size, 0);
100142

101143
if (php_random_bytes(bytes->val, size) == FAILURE) {
102144
zend_string_release(bytes);
103-
return;
145+
RETURN_FALSE;
104146
}
105147

148+
bytes->val[size] = '\0';
149+
106150
RETURN_STR(bytes);
107151
}
108152
/* }}} */
109153

110-
/* {{{ proto int random_int(int maximum)
154+
/* {{{ proto int random_int(int min, int max)
111155
Return an arbitrary pseudo-random integer */
112156
PHP_FUNCTION(random_int)
113157
{
114-
zend_long maximum;
115-
zend_long size;
116-
size_t i;
158+
zend_long min;
159+
zend_long max;
160+
zend_ulong limit;
161+
zend_ulong umax;
162+
zend_ulong result;
117163

118-
if (zend_parse_parameters(ZEND_NUM_ARGS(), "|l", &maximum) == FAILURE) {
164+
if (zend_parse_parameters(ZEND_NUM_ARGS(), "ll", &min, &max) == FAILURE) {
119165
return;
120166
}
121167

122-
if (ZEND_NUM_ARGS() == 0) {
123-
maximum = INT_MAX;
168+
if (min >= max) {
169+
php_error_docref(NULL, E_WARNING, "Minimum value must be less than the maximum value");
170+
RETURN_FALSE;
124171
}
125172

126-
if (maximum <= 0 || maximum > INT_MAX) {
127-
php_error_docref(NULL, E_WARNING, "Cannot use maximum less than 1 or greater than %d", INT_MAX);
173+
umax = max - min;
174+
175+
if (php_random_bytes(&result, sizeof(result)) == FAILURE) {
128176
RETURN_FALSE;
129177
}
130178

131-
long range = (long) maximum; // @todo Support min?
132-
133-
// Big thanks to @ircmaxell for the help on this bit
134-
union rand_long_buffer value;
135-
long result;
136-
int bits = (int) (log((double) range) / log(2.0)) + 1;
137-
int bytes = MAX(ceil(bits / 8), 1);
138-
long mask = (long) pow(2.0, (double) bits) - 1;
179+
// Special case where no modulus is required
180+
if (umax == ZEND_ULONG_MAX) {
181+
RETURN_LONG((zend_long)result);
182+
}
139183

140-
do {
141-
if (php_random_bytes(&value.buffer, 8) == FAILURE) {
142-
return;
184+
// Increment the max so the range is inclusive of max
185+
umax++;
186+
187+
// Powers of two are not biased
188+
if ((umax & ~umax) != umax) {
189+
// Ceiling under which ZEND_LONG_MAX % max == 0
190+
limit = ZEND_ULONG_MAX - (ZEND_ULONG_MAX % umax) - 1;
191+
192+
// Discard numbers over the limit to avoid modulo bias
193+
while (result > limit) {
194+
if (php_random_bytes(&result, sizeof(result)) == FAILURE) {
195+
return;
196+
}
143197
}
144-
result = value.number & mask;
145-
} while (result > maximum);
198+
}
146199

147-
RETURN_LONG(result);
200+
RETURN_LONG((zend_long)((result % umax) + min));
148201
}
149202
/* }}} */
150203

0 commit comments

Comments
 (0)