Skip to content

Commit 37410c4

Browse files
committed
Avoid RangeError on integers larger than LONG_LONG
1 parent 08f21e8 commit 37410c4

File tree

3 files changed

+89
-3
lines changed

3 files changed

+89
-3
lines changed

ext/mysql2/extconf.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ def asplode(lib)
1212
end
1313
end
1414

15+
# 2.1+
16+
have_func('rb_absint_size')
17+
have_func('rb_absint_singlebit_p')
18+
1519
# 2.0-only
1620
have_header('ruby/thread.h') && have_func('rb_thread_call_without_gvl', 'ruby/thread.h')
1721

@@ -20,6 +24,7 @@ def asplode(lib)
2024
have_func('rb_wait_for_single_fd')
2125
have_func('rb_hash_dup')
2226
have_func('rb_intern3')
27+
have_func('rb_big_cmp')
2328

2429
# borrowed from mysqlplus
2530
# http://github.com/oldmoe/mysqlplus/blob/master/ext/extconf.rb

ext/mysql2/statement.c

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ VALUE cMysql2Statement;
44
extern VALUE mMysql2, cMysql2Error, cBigDecimal, cDateTime, cDate;
55
static VALUE sym_stream, intern_new_with_args, intern_each;
66
static VALUE intern_usec, intern_sec, intern_min, intern_hour, intern_day, intern_month, intern_year, intern_to_s;
7+
#ifndef HAVE_RB_BIG_CMP
8+
static ID id_cmp;
9+
#endif
710

811
#define GET_STATEMENT(self) \
912
mysql_stmt_wrapper *stmt_wrapper; \
@@ -204,6 +207,47 @@ static void set_buffer_for_string(MYSQL_BIND* bind_buffer, unsigned long *length
204207
xfree(length_buffers); \
205208
}
206209

210+
/* return 0 if the given bignum can cast as LONG_LONG, otherwise 1 */
211+
static int my_big2ll(VALUE bignum, LONG_LONG *ptr)
212+
{
213+
unsigned LONG_LONG num;
214+
size_t len;
215+
#ifdef HAVE_RB_ABSINT_SIZE
216+
int nlz_bits = 0;
217+
len = rb_absint_size(bignum, &nlz_bits);
218+
#else
219+
len = RBIGNUM_LEN(bignum) * SIZEOF_BDIGITS;
220+
#endif
221+
if (len > 8) goto overflow;
222+
if (RBIGNUM_POSITIVE_P(bignum)) {
223+
num = rb_big2ull(bignum);
224+
if (num > LLONG_MAX)
225+
goto overflow;
226+
*ptr = num;
227+
}
228+
else {
229+
if (len == 8 &&
230+
#ifdef HAVE_RB_ABSINT_SIZE
231+
nlz_bits == 0 &&
232+
#endif
233+
#if defined(HAVE_RB_ABSINT_SIZE) && defined(HAVE_RB_ABSINT_SINGLEBIT_P)
234+
/* only -0x8000000000000000 is safe if `len == 8 && nlz_bits == 0` */
235+
!rb_absint_singlebit_p(bignum)
236+
#elif defined(HAVE_RB_BIG_CMP)
237+
rb_big_cmp(bignum, LL2NUM(LLONG_MIN)) == INT2FIX(-1)
238+
#else
239+
rb_funcall(bignum, intern_cmp, 1, LL2NUM(LLONG_MIN)) == INT2FIX(-1)
240+
#endif
241+
) {
242+
goto overflow;
243+
}
244+
*ptr = rb_big2ll(bignum);
245+
}
246+
return 0;
247+
overflow:
248+
return 1;
249+
}
250+
207251
/* call-seq: stmt.execute
208252
*
209253
* Executes the current prepared statement, returns +result+.
@@ -265,9 +309,23 @@ static VALUE execute(int argc, VALUE *argv, VALUE self) {
265309
#endif
266310
break;
267311
case T_BIGNUM:
268-
bind_buffers[i].buffer_type = MYSQL_TYPE_LONGLONG;
269-
bind_buffers[i].buffer = xmalloc(sizeof(long long int));
270-
*(LONG_LONG*)(bind_buffers[i].buffer) = rb_big2ll(argv[i]);
312+
{
313+
LONG_LONG num;
314+
if (my_big2ll(argv[i], &num) == 0) {
315+
bind_buffers[i].buffer_type = MYSQL_TYPE_LONGLONG;
316+
bind_buffers[i].buffer = xmalloc(sizeof(long long int));
317+
*(LONG_LONG*)(bind_buffers[i].buffer) = num;
318+
} else {
319+
/* The bignum was larger than we can fit in LONG_LONG, send it as a string */
320+
VALUE rb_val_as_string = rb_big2str(argv[i], 10);
321+
bind_buffers[i].buffer_type = MYSQL_TYPE_NEWDECIMAL;
322+
params_enc[i] = rb_val_as_string;
323+
#ifdef HAVE_RUBY_ENCODING_H
324+
params_enc[i] = rb_str_export_to_enc(params_enc[i], conn_enc);
325+
#endif
326+
set_buffer_for_string(&bind_buffers[i], &length_buffers[i], params_enc[i]);
327+
}
328+
}
271329
break;
272330
case T_FLOAT:
273331
bind_buffers[i].buffer_type = MYSQL_TYPE_DOUBLE;
@@ -501,4 +559,7 @@ void init_mysql2_statement() {
501559
intern_year = rb_intern("year");
502560

503561
intern_to_s = rb_intern("to_s");
562+
#ifndef HAVE_RB_BIG_CMP
563+
id_cmp = rb_intern("cmp");
564+
#endif
504565
}

spec/mysql2/statement_spec.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,26 @@
5959
expect(rows).to eq([{ "1" => 1 }])
6060
end
6161

62+
it "should handle bignum but in int64_t" do
63+
stmt = @client.prepare('SELECT ? AS max, ? AS min')
64+
int64_max = (1 << 63) - 1
65+
int64_min = -(1 << 63)
66+
result = stmt.execute(int64_max, int64_min)
67+
expect(result.to_a).to eq(['max' => int64_max, 'min' => int64_min])
68+
end
69+
70+
it "should handle bignum but beyond int64_t" do
71+
stmt = @client.prepare('SELECT ? AS max1, ? AS max2, ? AS max3, ? AS min1, ? AS min2, ? AS min3')
72+
int64_max1 = (1 << 63)
73+
int64_max2 = (1 << 64) - 1
74+
int64_max3 = 1 << 64
75+
int64_min1 = -(1 << 63) - 1
76+
int64_min2 = -(1 << 64) + 1
77+
int64_min3 = -0xC000000000000000
78+
result = stmt.execute(int64_max1, int64_max2, int64_max3, int64_min1, int64_min2, int64_min3)
79+
expect(result.to_a).to eq(['max1' => int64_max1.to_s, 'max2' => int64_max2.to_s, 'max3' => int64_max3.to_s, 'min1' => int64_min1.to_s, 'min2' => int64_min2.to_s, 'min3' => int64_min3.to_s])
80+
end
81+
6282
it "should keep its result after other query" do
6383
@client.query 'USE test'
6484
@client.query 'CREATE TABLE IF NOT EXISTS mysql2_stmt_q(a int)'

0 commit comments

Comments
 (0)