To repeat this bug, just execute the below code(I've run it with ruby185 and 186, you may need increase the complexity
of regexp if you has a high performance machine):
require "rfuzz/session"
include RFuzz
c = HttpClient.new("www.ruby-lang.org", 80)
r = c.get("/en/")
p = r.http_body
p.gsub!(/\r\n/, '')
s = "<div .*?span.*?<h3>.*?<dd>.*?<dl>.*?</dd>.*?</dl>"
timeout(1) {
p.gsub(Regexp.new(s)) {
}
}
This snippet code will be executed more than 1 second under windows with my old hp notebook, and it won't raise any
exception. I've traced the Ruby source, and I found it is because monitor thread(y thread in below code) won't get woken
up:
timeout.rb:
def timeout(sec, exception=Error)
return yield if sec == nil or sec.zero?
raise ThreadError, "timeout within critical session" if Thread.critical
begin
x = Thread.current
y = Thread.start {
sleep sec
x.raise exception, "execution expired" if x.alive?
}
yield sec
# return true
ensure
y.kill if y and y.alive?
end
end
The reason why y thread won't be woken up is because rb_thread_tick in CHECK_INTS isn't decreased THREAD_TICK(500) times,
so Y thread won't get scheduled until x finished:
rubysig.h
#define CHECK_INTS do {\
if (!(rb_prohibit_interrupt || rb_thread_critical)) {\
if (rb_thread_tick-- <= 0) {\
rb_thread_tick = THREAD_TICK;\
rb_thread_schedule();\
}\
}\
if (rb_trap_pending) rb_trap_exec();\
} while (0)
So, in fact, this is a thread schedule bug.
I hope I understand correctly and the above analytics can give you a little help. |