Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Request IDs not freed after fetching the resource #11

Open
jsdalton opened this issue May 6, 2021 · 1 comment
Open

Request IDs not freed after fetching the resource #11

jsdalton opened this issue May 6, 2021 · 1 comment
Assignees

Comments

@jsdalton
Copy link

jsdalton commented May 6, 2021

Due to a recently introduced change (33fb966), it would appear that that request IDs are no longer freed after a resource has been fetched.

The end result is that the cache of request IDs grows to its max size after about 64k DNS resolution requests, and the program is stuck in an infinite loop thereafter.

Details

Allocated request IDs are cleaned up after each request via DNS.free_request_id, which is called on each sender. However, 33fb966 introduces a change which deletes the sender after the request is made:

resolv/lib/resolv.rb

Lines 708 to 710 in f85979f

def sender_for(addr, msg)
@senders.delete([addr,msg.id])
end

This means, in the #close method, @senders is always empty for most Requesters:

resolv/lib/resolv.rb

Lines 782 to 792 in f85979f

def close
@mutex.synchronize {
if @initialized
super
@senders.each_key {|service, id|
DNS.free_request_id(service[0], service[1], id)
}
@initialized = false
end
}
end

Request ids are thus not getting deallocated.

The larger problem is that DNS.allocate_request_id is only capable of assigning a maximum of 64k request ids. Once the hash it uses to store these is fully populated, it goes into an infinite loop searching for a free slot that will never be filled.

The end result for users of this module is that after approximately 64k DNS requests, the program will halt execution in the middle of this while loop:

resolv/lib/resolv.rb

Lines 624 to 626 in f85979f

begin
id = random(0x0000..0xffff)
end while h[id]

Since the DNS::RequestID hash is stored as a class constant, instatiating a new instance of DNS will not solve the problem. The current workaround would be to manually flush values from the RequestID

Reproducing

It's fairly easy to demonstrate that the DNS::RequestID hash fills without ever being cleaned up.

The following script shows the length of the request ID cache growing linearly with each request. This script will also max at out 65536 (with some slow downs at higher numbers as it searches for available slots in the hash):

require 'resolv'

puts RUBY_VERSION
puts Resolv::DNS::RequestID
resolver = Resolv::DNS.new
domain = 'example.com'

i = 1
loop do
  resolver.getresources(domain, Resolv::DNS::Resource::IN::A)
  puts Resolv::DNS::RequestID.values.first.length
  i += 1
end
$ ruby thread_test.rb
2.7.3
{}
1
2
3
4
5
# ...

Impact

We recently ran into this issue in a long running script, which needs to re-resolve DNS names frequently. The script was halting execution and we could not figure out why. Debugging led us to the while loop in this module and to the problem described above.

Let me know if you have questions or difficulties reproducing the issue.

@stevenharman
Copy link

I think we can close this as of #9, yeah?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

3 participants