Skip to content

Unnecessary synchronization in org.xbill.DNS.Header::getID #215

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

Closed
maltalex opened this issue Sep 1, 2021 · 7 comments · Fixed by #216
Closed

Unnecessary synchronization in org.xbill.DNS.Header::getID #215

maltalex opened this issue Sep 1, 2021 · 7 comments · Fixed by #216
Milestone

Comments

@maltalex
Copy link
Contributor

maltalex commented Sep 1, 2021

I'm working on improving the performance of a project that deals with enormous amounts of dns messages. One of the performance bottlenecks I'm seeing is a whole lot of locking in org.xbill.DNS.Header::getID which is implemented like this:

private static final Random random = new SecureRandom();

public int getID() {
  synchronized (random) {
    if (id < 0) {
      id = random.nextInt(0xffff);
    }
    return id;
  }
}

The implementation is not only synchronized - it's synchronized around a static object, meaning that all getID calls in all Header instances are serial! Note that SecureRandom itself is thread-safe and doesn't require locking.

Could the getID implementation be changed to be lock-free using a volatile variable or an AtomicInteger? Or at least switched to use per-instance locking (e.g. synchronize on this or some other non-null field) to reduce the lock's scope?

I'd be happy to submit a PR with a lock-free implementation.

@ibauersachs
Copy link
Member

An AtomicInteger would generate predictable results, which is a no-go to avoid the Kaminsky attacks. Since SecureRandom is thread safe, the synchronization around it can be removed. And volatile would can be avoided by always generating an id (instead of -1) in the no-arg constructor.

If you open a PR, you may want to create it against the release/3.4.x branch as master won't get a new release soon.

@maltalex
Copy link
Contributor Author

maltalex commented Sep 1, 2021

I was suggesting using an AtomicInteger not for id generation, but for storing the id and setting it using an atomic, lock-free compareAndSet.

And volatile would can be avoided by always generating an id (instead of -1) in the no-arg constructor.

That would be the best option IMHO. But but then we lose the lazy initialization of the ID field, which looks like the intention of the author of that class. Using SecureRandom too much can also be extremely slow (when low on entropy), so if there are use cases that generate many Header instances but don't need to initialize their ID field, they'd see worse performance due to this change. Is that a common scenario?

@ibauersachs
Copy link
Member

I was suggesting using an AtomicInteger not for id generation, but for storing the id and setting it using an atomic, lock-free compareAndSet.

Ah, I see.

And volatile would can be avoided by always generating an id (instead of -1) in the no-arg constructor.

That would be the best option IMHO. But but then we lose the lazy initialization of the ID field, which looks like the intention of the author of that class. Using SecureRandom too much can also be extremely slow (when low on entropy), so if there are use cases that generate many Header instances but don't need to initialize their ID field, they'd see worse performance due to this change. Is that a common scenario?

The default implementation of SecureRandom is SHA1PRNG, which AFAIK doesn't do reseeds and thus will never run out of entropy. I wouldn't know when an unset ID would be useful. Answers coming from the network have an ID which is parsed, and sending request need an ID anyway. The only case where the ID must be statically 0 is DNS over HTTP since the protocol is TCP and the order of delivery is guaranteed.

@bwelling
Copy link
Collaborator

bwelling commented Sep 1, 2021

The reason for the lazy initialization, as far as I remember, was to avoid needing to generate a random ID that would immediately be overridden when parsing a header from wire format. It's possible that this is no longer needed; there is an alternate constructor that specifies the ID, and it's used when constructing a header from wire format (which is likely the common case; I'm not sure if there's any other code that separates the construction and parsing).

(and for the record, DoH doesn't require an ID of 0, it's a SHOULD. It's also not because of ordered delivery, it's because the use of a varying DNS ID can cause semantically equivalent DNS queries to be cached separately. With HTTP/2, there's no guarantee that responses will be ordered, but they will be correlated by HTTP stream.)

@maltalex
Copy link
Contributor Author

maltalex commented Sep 1, 2021

@ibauersachs SHA1PRNG is the default only in windows (see https://docs.oracle.com/javase/8/docs/technotes/guides/security/SunProviders.html#SecureRandomImp). On Linux, a slower default implementation is used.

Still, if we can agree that the ID has to be set in the constructor either via a parameter or using SecureRandom, then getID becomes just a simple getter with no synchronization.

maltalex added a commit to maltalex/dnsjava that referenced this issue Sep 2, 2021
Header::getID synchronized on a static field causing unnecessary locking
especially when the ID field didn't need to be generated. As per the
discussion in dnsjava#215, this was due to a desire to have the ID generation
be lazy, which is no longer needed.

This commit changes the ID generation behavior so the ID is either
explicitly supplied to the constructor, or generated by the constructor
using the same SecureRandom instance. This makes the getID method a
simple getter with no need for synchronization
@ibauersachs
Copy link
Member

Yes, you're right, I forgot about the DoH http-based caching. I don't have the project open ATM to search for references, but if parsing the message creates an empty Header instance, we should change that. But I assume that's already the case.

If the SecureRandom performance really turns out to be an issue I think we can revisit that later.

@maltalex
Copy link
Contributor Author

maltalex commented Sep 2, 2021

But I assume that's already the case.

It is. I took a glance at the parsing code before submitting the PR.

The Message constructors that take a byte[] or ByteBuffer call Header(DNSInput in), which in turn calls this(in.readU16());.
So it looks like the parsing flow creates the header with the ID from the wire.

ibauersachs pushed a commit that referenced this issue Sep 2, 2021
* removed synchronization in Header::getID

Header::getID synchronized on a static field causing unnecessary locking
especially when the ID field didn't need to be generated. As per the
discussion in #215, this was due to a desire to have the ID generation
be lazy, which is no longer needed.

This commit changes the ID generation behavior so the ID is either
explicitly supplied to the constructor, or generated by the constructor
using the same SecureRandom instance. This makes the getID method a
simple getter with no need for synchronization

* Header constructor - check that ID is in range

* fixed formatting
@ibauersachs ibauersachs linked a pull request Sep 12, 2021 that will close this issue
@ibauersachs ibauersachs added this to the v3.4.2 milestone Sep 19, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy