Skip to content

Commit de3dfb6

Browse files
committed
FIX: preserve NUL bytes in callback exceptions
Use the Ruby string length when forwarding callback exception messages to V8 so embedded NUL bytes do not truncate the message or leave the context deadlocked. Add regression coverage for same-thread and cross-thread callback exceptions with NUL-containing messages, and note the fix in the changelog.
1 parent 42a8a03 commit de3dfb6

3 files changed

Lines changed: 88 additions & 2 deletions

File tree

CHANGELOG

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
- Add `Context#perform_microtask_checkpoint` to synchronously drain the V8 microtask queue, useful for spec-compliant `dispatchEvent` sequencing inside Ruby callbacks
33
- Fix native memory leaks in `Context#heap_snapshot`/`Context#write_heap_snapshot`; thanks to Pranjali Thakur from depthfirst.com
44
- Fix large integral JavaScript numbers wrapping to negative Ruby integers; thanks to Pranjali Thakur from depthfirst.com
5+
- Fix Ruby callback exceptions with embedded NUL bytes permanently deadlocking a context; thanks to Pranjali Thakur from depthfirst.com
56

67
- 0.21.1 - 25-05-2026
78
- Run `:single_threaded` V8 dispatches on a reusable mini_racer-owned native thread so V8 does not execute on Ruby-owned threads

ext/mini_racer_extension/mini_racer_extension.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -988,9 +988,9 @@ static void *rendezvous_callback(void *arg)
988988
ser_init0(&s); // ruby exception pending
989989
w_byte(&s, 'e'); // send ruby error message to v8 thread
990990
r = rb_funcall(c->exception, rb_intern("to_s"), 0);
991-
err = StringValueCStr(r);
991+
err = StringValuePtr(r);
992992
if (err)
993-
w(&s, err, strlen(err));
993+
w(&s, err, RSTRING_LEN(r));
994994
goto out;
995995
}
996996

test/mini_racer_test.rb

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,91 @@ def test_attached_exceptions
226226
end
227227
end
228228

229+
def test_attached_exception_with_nul_message_does_not_deadlock_context
230+
require "open3"
231+
require "rbconfig"
232+
233+
script = <<~'RUBY'
234+
$stdout.sync = true
235+
$stderr.sync = true
236+
237+
Thread.new do
238+
sleep 10
239+
warn "child timed out"
240+
exit!(99)
241+
end
242+
243+
require "mini_racer"
244+
245+
NUL_PAYLOAD = "boom(\"a" + 0.chr + "b\")"
246+
247+
def assert_reusable(context, label)
248+
thread = Thread.new { context.eval("40 + 2") }
249+
unless thread.join(2)
250+
warn "#{label} reuse eval blocked"
251+
exit!(10)
252+
end
253+
254+
result = thread.value
255+
unless result == 42
256+
warn "#{label} reuse eval returned #{result.inspect}"
257+
exit!(11)
258+
end
259+
end
260+
261+
context = MiniRacer::Context.new
262+
context.attach("boom", proc { |x| raise(ArgumentError, x) })
263+
begin
264+
context.eval(NUL_PAYLOAD)
265+
warn "expected same-thread callback exception"
266+
exit!(12)
267+
rescue ArgumentError => e
268+
unless e.message == "a\x00b"
269+
warn "same-thread wrong exception message: #{e.message.inspect}"
270+
exit!(13)
271+
end
272+
end
273+
assert_reusable(context, "same-thread")
274+
275+
context = MiniRacer::Context.new
276+
context.attach("boom", proc { |x| raise(ArgumentError, x) })
277+
poison = Thread.new do
278+
begin
279+
context.eval(NUL_PAYLOAD)
280+
["no exception", nil]
281+
rescue => e
282+
[e.class.name, e.message]
283+
end
284+
end
285+
286+
unless poison.join(2)
287+
warn "cross-thread poison eval blocked"
288+
exit!(14)
289+
end
290+
291+
unless poison.value == ["ArgumentError", "a\x00b"]
292+
warn "cross-thread wrong exception: #{poison.value.inspect}"
293+
exit!(15)
294+
end
295+
assert_reusable(context, "cross-thread")
296+
RUBY
297+
298+
stdout, stderr, status = Open3.capture3(
299+
RbConfig.ruby,
300+
"-I#{File.expand_path("../lib", __dir__)}",
301+
"-e",
302+
script
303+
)
304+
305+
assert status.success?, <<~MSG
306+
NUL callback exception script failed with status #{status.exitstatus || "signal #{status.termsig}"}
307+
stdout:
308+
#{stdout}
309+
stderr:
310+
#{stderr}
311+
MSG
312+
end
313+
229314
def test_attached_on_object
230315
context = MiniRacer::Context.new
231316
context.eval "var minion"

0 commit comments

Comments
 (0)