Skip to content

Conversation

victorherrerod
Copy link

Resolves #32

if result == 0 {
continue
}
if result == 1 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it always 1 and not any positive number?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to poll man page, it's the number of elements in the pollFDs array that are ready for I/O. The array being of size 1, it could not be anything other than 1 if it is a positive number.

I can change it to >=, but that should never happen.

var pollFDs = [pollfd(fd: serviceSockFD, events: Int16(POLLIN), revents: 0)]
while true {
guard !Task.isCancelled else {
return continuation.finish(throwing: CancellationError())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it would be more useful to throw AsyncDNSResolver.Error here? (we would need to add .cancelled to the enum)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I thought that CancellationError was the standard way to throw when a task is cancelled, but apparently it's not a guarantee and an AsyncDNSResolver.Error might be more specific and useful.

@yim-lee
Copy link
Member

yim-lee commented Mar 22, 2024

@swift-server-bot add to allowlist

@yim-lee yim-lee requested review from weissi, Lukasa and ktoso March 22, 2024 17:58
Add AsyncDNSResolver.Error.cancelled and error messages when polling the DNSSD service socket
Copy link
Member

@weissi weissi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like a kernel thread blocking function in the middle of an asynchronous function

return continuation.finish(throwing: AsyncDNSResolver.Error(code: .internalError, message: "Failed to access the DNSSD service socket"))
}

var pollFDs = [pollfd(fd: serviceSockFD, events: Int16(POLLIN), revents: 0)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

poll is blocking until this is ready so we can't do this here. If you want async I/O you'd need to use SwiftNIO here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We did work to remove the SwiftNIO dependency earlier (#20), so I don't think we want to add it back.

The intention here is to allow checking for task cancellation, because calling DNSServiceProcessResult alone just blocks and doesn't seem to give us the chance to do that at all.

The docs for DNSServiceProcessResult says:

This call will block until the daemon's response is received. Use DNSServiceRefSockFD() in
conjunction with a run loop or select() to determine the presence of a response from the
server before calling this function to process the reply without blocking.

That's why we went with this approach here. It doesn't sound like we must do async I/O to see improvement, but I could be wrong of course.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If poll isn't called with a timeout, it'll block. If we mustn't use NIO, prefer DispatchSources.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use DNSServiceRefSockFD() in conjunction with a run loop

^^ that's exactly what NIO can do for you.

What's the goal with removing the NIO dependency? Every adopter will have a NIO dependency anyway, right?

Copy link
Contributor

@glbrntt glbrntt Mar 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the goal with removing the NIO dependency?

Only a couple of methods on ByteBuffer were being used for parsing so pulling in NIO was quite heavy handed. Replacing with ArraySlice<UInt8> was relatively straightforward. There's no requirement to be free of NIO.

Every adopter will have a NIO dependency anyway, right?

Not necessarily although I'm sure many will.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If poll isn't called with a timeout, it'll block.

We are passing 0 as timeout I believe (poll(&pollFDs, 1, 0)), so it should return immediately according to the man page:

Specifying a negative value in timeout means an infinite timeout.  
Specifying a timeout of zero causes poll() to return immediately, 
even if no file descriptors are ready.

I wonder if it should sleep a bit before calling poll again though.

that's exactly what NIO can do for you

Is there an example that we can follow? I found these threads:

But I didn't find anything obvious.

I don't doubt that SwiftNIO can do this more efficiently, but at the end of the day we need to look at the trade-offs. Does doing it without SwiftNIO give us a "good enough" solution? Is it worthwhile to add SwiftNIO, which is a heavier dependency, for this? (I don't have problem with adding the SwiftNIO dependency FWIW.)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hot-looping on poll is not the right choice: it's not meaningfully different than just sleeping the thread, as we'll never yield it.

Doing this with NIO directly is gonna be a little painful as you'd have to write a Channel to intercept the readFromSocket function. Not the end of the world, but not as easy as you might like. I continue to think that DispatchSource is going to be the easiest way to achieve this goal.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yes, if you don't actually need to do anything with the data in there, then DispacthSource is good. It's pretty bad if you need to actually read the data but if you just need to be told that there is something to read, DispatchSource.makeReadSource is fine

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your feedback, I'm going to try implementing this with DispatchSource then, if it's ok for everyone.

Convert DNSSD to class to use deinit on the pointers we initialized for dns-sd C functions
The Dispatch Source event handler is called when data can be read from the DNSSD service socket
Remove the unchecked Sendable subtyping for the DNSServiceRef pointer as it's no longer needed
Remove the cancelled case from the error code enum as it's no longer needed
@victorherrerod
Copy link
Author

Hi, sorry for the wait, I updated the implementation to use DispatchReadSource to process the query results.

I converted the DNSSD struct to a class to use the deinit and deinitialize the pointers when we are done. The current implementation deinitialized the pointers at the end of the query function, which caused a crash when calling DNSServiceProcessResult from the event handler.
Let me know if converting it to a class is OK or if we should change it.

Other than that I only removed code that was no longer necessary.

@victorherrerod
Copy link
Author

victorherrerod commented Apr 9, 2024

Don't bother reviewing for now as the code is actually not correct, we only have one reply handler pointer so concurrent requests cause a crash. Trying to fix it.

Convert DNSSD back to struct
A different Query class is created for every query, so concurrent queries no longer cause an issue
@victorherrerod
Copy link
Author

I added a Query class to handle the pointer initialization and deallocation, now a Query instance is created for every query made and the deallocation happens in the deinit of each instance.

I have tested with many concurrent requests and it seems to be working correctly. This implementation uses DispatchSource, as stated above.

Let me know if there is any changes I should make!

@yim-lee
Copy link
Member

yim-lee commented Apr 10, 2024

Thanks @victorherrerod.

@weissi @Lukasa @ktoso Can you please review the latest changes when you have time? Thanks in advance.

@yim-lee yim-lee requested review from weissi and glbrntt April 10, 2024 19:00
byteCount: MemoryLayout<QueryReplyHandler>.stride,
alignment: MemoryLayout<QueryReplyHandler>.alignment
)
init(handler: QueryReplyHandler) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: add newline before

let replyHandlerPointer = UnsafeMutableRawPointer.allocate(
byteCount: MemoryLayout<QueryReplyHandler>.stride,
alignment: MemoryLayout<QueryReplyHandler>.alignment
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a difference in initializing serviceRefPtr and replyHandlerPointer here vs. inside the initializer? Does anyone have any preference?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Slight preference towards doing it in the init so the allocate and initializeMemory calls are next to each other.

let replyHandlerPointer = UnsafeMutableRawPointer.allocate(
byteCount: MemoryLayout<QueryReplyHandler>.stride,
alignment: MemoryLayout<QueryReplyHandler>.alignment
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Slight preference towards doing it in the init so the allocate and initializeMemory calls are next to each other.

return continuation.finish(throwing: AsyncDNSResolver.Error(code: .internalError, message: "Failed to access the DNSSD service socket"))
}

let readSource = DispatchSource.makeReadSource(fileDescriptor: serviceSockFD)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be passing our own queue here rather than using the default?

Copy link
Member

@weissi weissi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • security relevant use after free bug
  • plus: either file descriptor leak or use after free


continuation.onTermination = { _ in
readSource.cancel()
DNSServiceRefDeallocate(query.serviceRefPtr.pointee)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security relevant use after free bug here, you could be concurrently deallocating this as well as using it in the event handler

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The deallocation is now done in the dispatch source cancel handler so that should no longer happen

// Streaming done
continuation.finish()
// Streaming done
continuation.finish()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should cancel the read source

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied in latest commit

return continuation.finish(throwing: AsyncDNSResolver.Error(code: .internalError, message: "Failed to access the DNSSD service socket"))
}

let readSource = DispatchSource.makeReadSource(fileDescriptor: serviceSockFD)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't this file descriptor leaked?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I close it now in the cancellation handler

return continuation.finish(throwing: AsyncDNSResolver.Error(code: .internalError, message: "Failed to access the DNSSD service socket"))
}

let readSource = DispatchSource.makeReadSource(fileDescriptor: serviceSockFD)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you'll need .setCancelHandler here and manage the resources from there.

DispatchSources that use file descriptors and don't use setCancelHandler are guaranteed to be incorrect. Also see https://developer.apple.com/documentation/dispatch/1385648-dispatch_source_set_cancel_handl?language=objc#discussion

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, thanks for the feedback! I implemented it in the latest commit

DNSServiceRefDeallocate is called in the DispatchSource cancellation handler to be sure it's not used in the event handler after being freed
Remove Query class as it is not needed for pointer management
Close DNSServiceRefSockFD in DispatchSource cancellation handler to avoid any FD leak
@victorherrerod
Copy link
Author

Thanks everyone for the feedback, I followed @weissi advice and implemented a cancellation handler for the dispatch source that manages the ressources. I also deleted the Query class, which was kind of a hack to handle the pointers deallocation.

I have tested concurrent queries and everything seems to work correctly. Unit tests all passed too.
Let me know if there are other changes that have to be made.

@yim-lee yim-lee requested review from weissi and glbrntt April 18, 2024 15:50
DNSServiceRefDeallocate(serviceRefPtr.pointee)
let serviceSockFD = DNSServiceRefSockFD(serviceRefPointer.pointee)
guard serviceSockFD != -1 else {
return continuation.finish(throwing: AsyncDNSResolver.Error(code: .internalError, message: "Failed to access the DNSSD service socket"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aren't we leaking the serviceRefPointer and replyHandlerPointer in this branch?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the code so that the deallocates are also called in this branch and the other error branch just above.

@victorherrerod
Copy link
Author

Hi, could someone review the changes? We'd like to use this in a project to update our SRV resolver to use async/await, and being able to timeout the requests by cancelling them is necessary.

I've tried to answer and fix most of the issues, let me know if there is anything else that's needed.


// Wrap 'handler' into a pointer so we can pass it to DNSServiceQueryRecord
let replyHandlerPointer = UnsafeMutablePointer<QueryReplyHandler>.allocate(capacity: 1)
replyHandlerPointer.initialize(repeating: handler, count: 1)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's avoid initialize(repeating:), it's not needed. Just initialize(to:) is fine.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in latest commit

@@ -173,6 +180,12 @@ struct DNSSD {

return try replyHandler.generateReply(records: records)
}

private func deallocatePointers(serviceRefPointer: UnsafeMutablePointer<DNSServiceRef?>, replyHandlerPointer: UnsafeMutablePointer<DNSSD.QueryReplyHandler>) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Making this static would make the code a lot easier to follow.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made it static in the latest commit

@victorherrerod
Copy link
Author

Anything new for this PR? It would be nice to be able to use it in a project, and with the requests being uncancellable and the timeout being so long, it's kind of difficult to use.
I understand that it's still a pretty new lib that may not be prod-ready, so no pressure 🙌

@XfableX
Copy link

XfableX commented Jan 15, 2025

Hey all any updates to this one? Would love to use this in some projects but the un-cancellable 30 second timeout is brutal.

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

Successfully merging this pull request may close these issues.

Make DNSSD queries cancellable
6 participants