Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
- vNext
- Fix building with Clang 22+ and big-endian bigint serialization by making bigint serialization byte-oriented instead of relying on native `uint64_t`/`unsigned long` representations
- Fix Ruby integers at or above 512 bits being silently truncated when passed to JavaScript, and large JavaScript bigints producing an invalid internal value when returned to Ruby
- Support Ruby and JavaScript bigints up to a 16 MiB magnitude, using allocation-free conversion for common sizes and bounded dynamic storage for larger values

- 0.22.0 - 12-08-2026
- Add `Context#call_await` and `Context#eval_await`: like `call`/`eval` but block until a returned Promise settles and return the settled value; rejections raise `MiniRacer::RuntimeError`
- Fix a `call` or `eval` made from a Ruby callback taking the timeout or `stop` meant for the evaluation around it, which then kept running
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ puts context.eval("array_and_hash()")
# => {"a" => 1, "b" => [1, {"a" => 1}]}
```

Ruby `Integer` and JavaScript `BigInt` values are converted exactly up to a
16 MiB magnitude (about 134 million bits). Larger individual values are
rejected with a serialization error rather than truncated.

### Return binary data from Ruby to JavaScript

Attached Ruby functions can return binary data as `Uint8Array` using `MiniRacer::Binary`:
Expand Down
79 changes: 52 additions & 27 deletions ext/mini_racer_extension/mini_racer_extension.c
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ static inline void rb_thread_lock_native_thread(void)

#define countof(x) (sizeof(x) / sizeof(*(x)))
#define endof(x) ((x) + countof(x))
#define BIGINT_STACK_WORDS 64
#define BIGINT_MAX_BYTES (16 * 1024 * 1024)

// mostly RO: assigned once by platform_set_flag1 while holding |flags_mtx|,
// from then on read-only and accessible without holding locks
Expand Down Expand Up @@ -353,33 +355,32 @@ static void des_date(void *arg, double v)
put(arg, rb_time_new(sec, usec));
}

// note: v8 stores bigints in 1's complement, ruby in 2's complement,
// so we have to take additional steps to ensure correct conversion
// note: v8 stores bigints as a sign plus little-endian 64-bit magnitude words
static void des_bigint(void *arg, const void *p, size_t n, int sign)
{
VALUE v;
size_t i;
DesCtx *c;
unsigned long *a, t, limbs[65]; // +1 to suppress sign extension
int flags;

c = arg;
if (*c->err)
return;
if (n > sizeof(limbs) - sizeof(*limbs)) {
if (n % sizeof(uint64_t)) {
snprintf(c->err, sizeof(c->err), "bad bigint");
return;
}
if (n > BIGINT_MAX_BYTES) {
snprintf(c->err, sizeof(c->err), "bigint too big");
return;
}
a = limbs;
t = 0;
for (i = 0; i < n; a++, i += sizeof(*a)) {
memcpy(a, (char *)p + i, sizeof(*a));
t = *a;
if (n == 0) {
v = INT2FIX(0);
} else {
flags = INTEGER_PACK_LITTLE_ENDIAN;
if (sign < 0)
flags |= INTEGER_PACK_NEGATIVE;
v = rb_integer_unpack(p, n/sizeof(uint64_t), sizeof(uint64_t), 0, flags);
}
if (t >> 63)
*a++ = 0; // suppress sign extension
v = rb_big_unpack(limbs, a-limbs);
if (sign < 0)
v = rb_funcall(v, rb_intern("-@"), 0);
put(c, v);
}

Expand Down Expand Up @@ -580,12 +581,42 @@ static void add_string(Ser *s, VALUE v)
return ser_string(s, p, n);
}

// Keep small values allocation-free while allowing large values up to a
// deliberate per-value limit that bounds temporary conversion storage.
static int serialize_bigint(Ser *s, VALUE v)
{
uint64_t stack_words[BIGINT_STACK_WORDS];
uint64_t *words;
size_t nwords, nbytes;
int packed;

nwords = rb_absint_numwords(v, 64, NULL);
if (nwords == (size_t)-1 || nwords > BIGINT_MAX_BYTES/sizeof(*words))
return bail(&s->err, "bigint too big");
nbytes = nwords * sizeof(*words);
words = stack_words;
if (nwords > countof(stack_words)) {
words = malloc(nbytes);
if (!words)
return bail(&s->err, "out of memory");
}
packed = rb_integer_pack(v, words, nwords, sizeof(*words), 0,
INTEGER_PACK_LITTLE_ENDIAN);
if (packed < -1 || packed > 1) {
if (words != stack_words)
free(words);
return bail(&s->err, "bigint too big");
}
ser_bigint(s, words, nbytes, packed < 0 ? -1 : 1);
if (words != stack_words)
free(words);
return *s->err ? -1 : 0;
}

static int serialize1(Ser *s, VALUE refs, VALUE v)
{
unsigned long limbs[64];
VALUE a, t, id;
size_t i, n;
int sign;

if (*s->err)
return -1;
Expand Down Expand Up @@ -670,15 +701,7 @@ static int serialize1(Ser *s, VALUE refs, VALUE v)
ser_bool(s, 0);
break;
case T_BIGNUM:
// note: v8 stores bigints in 1's complement, ruby in 2's complement,
// so we have to take additional steps to ensure correct conversion
memset(limbs, 0, sizeof(limbs));
sign = rb_big_sign(v) ? 1 : -1;
if (sign < 0)
v = rb_big_mul(v, LONG2FIX(-1));
rb_big_pack(v, limbs, countof(limbs));
ser_bigint(s, limbs, countof(limbs), sign);
break;
return serialize_bigint(s, v);
case T_FIXNUM:
ser_int(s, FIX2LONG(v));
break;
Expand Down Expand Up @@ -958,6 +981,8 @@ static VALUE deserialize1(DesCtx *d, const uint8_t *p, size_t n)

if (des(&err, p, n, d))
rb_raise(runtime_error, "%s", err);
if (*d->err)
rb_raise(runtime_error, "%s", d->err);
if (d->tos != d->stack) // should not happen
rb_raise(runtime_error, "parse stack not empty");
return d->tos->a;
Expand Down Expand Up @@ -1020,7 +1045,7 @@ static void *rendezvous_callback(void *arg)
goto fail;
}
ser_init1(&s, 'c'); // callback reply
if (serialize(&s, r)) { // should not happen
if (serialize(&s, r)) {
c->exception = rb_exc_new_cstr(internal_error, s.err);
ser_reset(&s);
goto fail;
Expand Down
27 changes: 17 additions & 10 deletions ext/mini_racer_extension/serde.c
Original file line number Diff line number Diff line change
Expand Up @@ -243,33 +243,38 @@ static void ser_num(Ser *s, double v)
}
}

// ser_bigint: |n| is in bytes, not quadwords
static void ser_bigint(Ser *s, const uint64_t *p, size_t n, int sign)
// ser_bigint: |p| points to |n| bytes, interpreted as little-endian
// 64-bit words. Keep the interface byte-oriented so callers don't need to
// expose a concrete word type.
static void ser_bigint(Ser *s, const void *p, size_t n, int sign)
{
const uint8_t *bytes;

if (*s->err)
return;
if (n % 8) {
snprintf(s->err, sizeof(s->err), "bad bigint");
return;
}
bytes = p;
w_byte(s, 'Z');
// chop off high all-zero words
n /= 8;
while (n--)
if (p[n])
break;
if (n == (size_t)-1) {
while (n > 0 && bytes[n-1] == 0)
n--;
if (n == 0) {
w_byte(s, 0); // normalized zero
} else {
n = 8*n + 8;
n = (n + 7) & ~(size_t)7;
w_varint(s, 2*n + (sign < 0));
w(s, p, n);
w(s, bytes, n);
}
}

static void ser_int(Ser *s, int64_t v)
{
uint8_t bytes[8];
uint64_t t;
size_t i;
int sign;

if (*s->err)
Expand All @@ -279,8 +284,10 @@ static void ser_int(Ser *s, int64_t v)
if (v <= INT64_MAX/1024)
return ser_num(s, v);
t = v < 0 ? (uint64_t)(-(v + 1)) + 1 : (uint64_t)v;
for (i = 0; i < sizeof(bytes); i++)
bytes[i] = t >> (8*i);
sign = v < 0 ? -1 : 1;
ser_bigint(s, &t, sizeof(t), sign);
ser_bigint(s, bytes, sizeof(bytes), sign);
} else {
w_byte(s, 'I');
w_zigzag(s, v);
Expand Down
89 changes: 89 additions & 0 deletions test/mini_racer_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1681,6 +1681,95 @@ def test_large_integer
end
end

def test_fixnum_bigint_serialization
if RUBY_ENGINE == "truffleruby"
skip "C extension is not used on TruffleRuby"
end

[-(2**62), (2**62) - 1].each do |integer|
context = MiniRacer::Context.new
context.attach("test", proc { integer })

assert_equal "bigint", context.eval("typeof test()")
assert_equal integer.to_s, context.eval("test().toString()")
assert_equal integer, context.eval("test()")
end
end

def test_large_bigint_serialization_uses_all_packed_limbs
if RUBY_ENGINE == "truffleruby"
skip "C extension is not used on TruffleRuby"
end

[
(2**64) - 1,
-((2**64) - 1),
2**64,
-(2**64),
(2**128) + (2**64) + 12_345,
-((2**128) + (2**64) + 12_345),
2**512,
-((2**512) + 1),
(2**1024) + (2**512) + 1,
-((2**1024) + (2**512) + 1),
(2**4095) + (2**2048) + 17,
-((2**4095) + (2**2048) + 17),
(2**4096) + (2**2048) + 17,
-((2**4096) + (2**2048) + 17),
(2**32_768) + (2**16_384) + 17,
-((2**32_768) + (2**16_384) + 17)
].each do |big_int|
context = MiniRacer::Context.new
context.attach("test", proc { big_int })

assert_equal "bigint", context.eval("typeof test()")
assert_equal big_int.to_s, context.eval("test().toString()")
assert_equal big_int, context.eval("test()")
end
end

def test_v8_bigint_deserialization_handles_zero_and_large_nested_values
if RUBY_ENGINE == "truffleruby"
skip "C extension is not used on TruffleRuby"
end

context = MiniRacer::Context.new
expected = (2**32_768) + (2**16_384) + 17

assert_equal 0, context.eval("0n")
assert_equal((2**64) - 1, context.eval("(2n ** 64n) - 1n"))
assert_equal expected, context.eval("(2n ** 32768n) + (2n ** 16384n) + 17n")
assert_equal(
-expected,
context.eval("-((2n ** 32768n) + (2n ** 16384n) + 17n)")
)
assert_equal [expected],
context.eval("[(2n ** 32768n) + (2n ** 16384n) + 17n]")
end

def test_bigint_bridge_rejects_values_larger_than_dynamic_limit
if RUBY_ENGINE == "truffleruby"
skip "C extension is not used on TruffleRuby"
end

context = MiniRacer::Context.new
max_bigint_bytes = 16 * 1024 * 1024 # BIGINT_MAX_BYTES in the C extension
first_rejected_bit = max_bigint_bytes * 8
too_big = 1 << first_rejected_bit
context.attach("test", proc { too_big })

error = assert_raises(MiniRacer::InternalError) { context.eval("test()") }
assert_equal "bigint too big", error.message
assert_equal 2, context.eval("1 + 1")

error =
assert_raises(MiniRacer::RuntimeError) do
context.eval("1n << #{first_rejected_bit}n")
end
assert_equal "bigint too big", error.message
assert_equal 2, context.eval("1 + 1")
end

def test_uint8array_is_converted_to_string
context = MiniRacer::Context.new
result = context.eval("new Uint8Array([0, 1, 2, 3])")
Expand Down