Skip to content

Implement bestEffort functions for hashable and equatable. #280

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

Merged
merged 10 commits into from
May 5, 2021

Conversation

madsodgaard
Copy link
Contributor

@madsodgaard madsodgaard commented Mar 13, 2021

This PR adds bestEffortHash and bestEffortEquals in order to provide best effort method for using Hashable and Equatable in other projects. This is useful/needed to solve a problem such as swift-server/async-http-client#330

@swift-server-bot
Copy link

Can one of the admins verify this patch?

11 similar comments
@swift-server-bot
Copy link

Can one of the admins verify this patch?

@swift-server-bot
Copy link

Can one of the admins verify this patch?

@swift-server-bot
Copy link

Can one of the admins verify this patch?

@swift-server-bot
Copy link

Can one of the admins verify this patch?

@swift-server-bot
Copy link

Can one of the admins verify this patch?

@swift-server-bot
Copy link

Can one of the admins verify this patch?

@swift-server-bot
Copy link

Can one of the admins verify this patch?

@swift-server-bot
Copy link

Can one of the admins verify this patch?

@swift-server-bot
Copy link

Can one of the admins verify this patch?

@swift-server-bot
Copy link

Can one of the admins verify this patch?

@swift-server-bot
Copy link

Can one of the admins verify this patch?

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.

Thank you. That’s a good start! We’ll need to fix the equals method and need some tests too.

@weissi weissi requested a review from Lukasa March 13, 2021 15:07
@madsodgaard madsodgaard requested a review from weissi March 13, 2021 19:00
@glbrntt
Copy link
Contributor

glbrntt commented Mar 15, 2021

@swift-nio-bot test this please

@glbrntt glbrntt added the 🆕 semver/minor Adds new public API. label Mar 15, 2021
Copy link
Contributor

@Lukasa Lukasa left a comment

Choose a reason for hiding this comment

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

Hmm, I'm uncertain about whether it's a good idea to define equatability and hashability for this object in a way that does not include the key log callback. This seems likely to cause surprising behaviours in cases where the type is used as a key.

I'm inclined to suggest we should solve the problem in async-http-client a different way, rather than using this incomplete and surprising definition for equatability and hashability.

@madsodgaard
Copy link
Contributor Author

What are the alternative ways of solving this problem? If TLSConfiguration is not Hashable we can't use it in our key for the connection pool, if I understand correctly? I'd be happy to try another way, just not sure what that way could be, other than not utilizing the pool.

@Lukasa
Copy link
Contributor

Lukasa commented Mar 15, 2021

Use something that isn't TLSConfiguration but that either can be derived from it or that can derive it.

In this case it seems like if we wanted to do the least effort possible we could just put these definitions on a wrapper structure whose equality is defined this way. However, a better move is probably to have AHC have its own notion of TLS configuration not directly tied to swift-nio-ssl's, not least given that it works with multiple TLS backends anyway.

@weissi
Copy link
Member

weissi commented Mar 16, 2021

@Lukasa / @madsodgaard I agree that we should only hash in here if we can include all callbacks, we definitely can't just compare everything but the callbacks. If the callbacks are all C callbacks, then I'm happy comparing their pointer address. Cory wdyt?

@madsodgaard what Cory suggests is what I meant with if we can't reasonably hash it in here, then we can hack around it in AHC. The simplest way (in AHC) is struct TLSConfigurationHashableHack: Hashable { var tlsConfigureation: TLSConfiguration }. Then you can define hashable on TLSConfigurationHashableHack.

@madsodgaard
Copy link
Contributor Author

@weissi Oh okay. And then it would be fine that we don't include the callbacks in AHC's definition of what Hashable is for TLSConfiguration(Wrapper)?

@weissi
Copy link
Member

weissi commented Mar 16, 2021

@madsodgaard tbh, I don't think we can just not hash the callbacks at all. There can conceivably two distinct TLSConfiguration that only differ in the callbacks. And the callbacks are very meaningful because that's where the user tells us whether to trust a cert or not. It would be really bad if the user configured an "always accept" callback that would then get reused for something else.

@Lukasa
Copy link
Contributor

Lukasa commented Mar 16, 2021

The callbacks are not C callbacks, they're Swift callbacks.

@weissi
Copy link
Member

weissi commented Mar 16, 2021

The callbacks are not C callbacks, they're Swift callbacks.

@Lukasa / @madsodgaard Okay, then we have basically one option: hash/compare the bytes of the closure

withUnsafeBytes(of: &self.theClosure) { closureBits in
    var hasher = Hasher()
    closureBits.forEach { hasher.combine($0) }
}

@Lukasa wdyt?

@Lukasa
Copy link
Contributor

Lukasa commented Mar 17, 2021

I remain uncertain of why we think we have to do this, for two reasons.

Firstly, AHC has already made the mistake of trying to support two TLS implementations using the configuration for only one of them. This is a bad decision, and we shouldn't propagate it further. AHC should have a general TLS configuration object that it can use to derive the specific ones, and that object absolutely can be hashable.

Secondly, even if we disregarded that, there is only one callback on TLSConfiguration and it's keyLogCallback, used to implement SSLKEYLOGFILE support. Do we really need to drop into unsafe code to add hashability to this type for that?

@weissi
Copy link
Member

weissi commented Mar 17, 2021

I remain uncertain of why we think we have to do this, for two reasons.

The upsides of doing it in NIOSSL are:

  • NIOSSL knows exactly what's part of TLSConfiguration (in the latest version), it's harder to forget to add a newly added member to the equals/hashable implementations
  • NIOSSL has to deal with unsafe code already so arguably it's the best place to do it.

One other option of doing it inside NIOSSL is to instead of implementing Hashable, exporting two functions:

extension TLSConfiguration {
    // will say no to two unequals, tries to say yes to two equals but can't guarantee no false negatives
    public func bestEffortIsEqualTo(_ other: TLSConfiguration) -> Bool {
       ...
    }

    // two objects that are equal wrt `bestEffortIsEqualTo` are guaranteed to yield the same hash, others do not.
    public func bestEffortHash(into: inout Hasher) -> UInt64 {
        ...
    }
}

WDYT?

Firstly, AHC has already made the mistake of trying to support two TLS implementations using the configuration for only one of them. This is a bad decision, and we shouldn't propagate it further. AHC should have a general TLS configuration object that it can use to derive the specific ones, and that object absolutely can be hashable.

Secondly, even if we disregarded that, there is only one callback on TLSConfiguration and it's keyLogCallback, used to implement SSLKEYLOGFILE support. Do we really need to drop into unsafe code to add hashability to this type for that?

Fair enough, @madsodgaard in that case I'd suggest to move this code into an internal type into AHC which does

internal struct HashableTLSConfiguration {
   var tlsConfiguration: TLSConfiguration
}

extension HashableTLSConfiguration: Hashable { 
   ...
}

and then inside AHC, we'd just put a HashableTLSConfiguration into the connection pool instead of a TLSConfiguration.

If @Lukasa thinks the bestEffortIsEqualTo/HashCode here is a good idea, then we still need those types in AHC but they'd just need to call into the best effort functions.

@weissi
Copy link
Member

weissi commented Mar 20, 2021

friendly ping @Lukasa on the bestEffort* in this repo

@Lukasa
Copy link
Contributor

Lukasa commented Mar 22, 2021

I'm not sure that we gain much from the bestEffort implementations tbqh.

@weissi
Copy link
Member

weissi commented Mar 23, 2021

I'm not sure that we gain much from the bestEffort implementations tbqh.

But how are the dependencies supposed to hash/compare the right things? We'll add things to the TLS config in the future and then things will start to subtle break. So I think NIOSSL can definitely provide the best fidelity here (and IMHO should). But I agree with you that we shouldn't do it with Hashable because it's only a best effort.

@Lukasa
Copy link
Contributor

Lukasa commented Mar 23, 2021

The question remains why dependencies are hashing the TLS configuration at all.

@weissi
Copy link
Member

weissi commented Mar 23, 2021

The question remains why dependencies are hashing the TLS configuration at all.

For connection pools, we need to be able to compare the TLS connection of the connection in the pool with the TLS connection that the user would like for the request. False negatives are fine, we can always create a new connection.

That means we need (best effort) comparison (so the user can create an internal type wrapping the TLS config that adds Equatable). And because it's kinda nice to store the pool in a dictionary so they'd need to make it hashable.

@Lukasa
Copy link
Contributor

Lukasa commented Mar 23, 2021

Yes, I understand the proposed design. My question is whether that proposed design is good.

Specifically, should libraries encapsulating swift-nio-ssl pervasively expose TLSConfiguration as the mechanism by which they configure TLS. My general answer is no, they should not, because it couples them tightly to their TLS implementation. If users instead provide an encapsulating general-purpose TLS configuration that they use to generate a swift-nio-ssl TLSConfiguration object, only the general-purpose one needs to be Hashable. That seems to me to be a vastly preferable design, no?

@weissi
Copy link
Member

weissi commented Mar 23, 2021

Specifically, should libraries encapsulating swift-nio-ssl pervasively expose TLSConfiguration as the mechanism by which they configure TLS. My general answer is no, they should not, because it couples them tightly to their TLS implementation.

I see! I agree with you here.

That seems to me to be a vastly preferable design, no?

Absolutely. I do however think that evangelism and not preventing the best effort eq/hash APIs is the best way to get there. Fact is that there are libraries that use NIOSSL.TLSConfiguration today and I think we should allow them (through the best effort eq/hash APIs) to solve their problem today. I'm happy with adding a - warning: If you need this, then you are probably doing ... but shouldn't.

This is compounded by the fact that creating a TLS configuration object that can then yield a NIOSSL.TLSConfiguration/Security.TLSOptions/Windows equivalent isn't actually super easy and there's (AFAIK) no prior art in Swift. So not offering the proposed APIs leaves the authors of those libraries in a tough spot.

Short of now offering connection pools, they only really replicate the eq/hash functionality in their own packages which I think may be at a risk of security vulnerabilities because we may add properties that they miss. That could make their hand-rolled eq/hash functionality have false positives (claiming two TLSConfigs are equal when they're not) and that's bad.

@Lukasa
Copy link
Contributor

Lukasa commented Mar 23, 2021

I think if we think we should offer this, then we should go whole hog and use withUnsafeBytes(of:) the closure.

@weissi
Copy link
Member

weissi commented Mar 23, 2021

I think if we think we should offer this, then we should go whole hog and use withUnsafeBytes(of:) the closure.

Works for me. I'm mildly worried about ASan complaining that we read bytes that were never written (padding) but I'm totally happy deferring that to when/if we actually hit this issue.

Sorry, ignore the above. I'm happy with this. I thought you meant withUnsafeBytes(of:) the whole TLSConfiguration.

@madsodgaard madsodgaard changed the title Make TLSConfiguration Hashable Implement bestEffort functions for hashable and equatable. Apr 24, 2021
@madsodgaard
Copy link
Contributor Author

@Lukasa @weissi I implemented the bestEffort* functions from what I understood from your conclusion. I added some documentation/warnings, but let me know what you think is appropriate to put there!

///
/// - warning: You should probably not use this function. This function can return false-negatives, but not false-positives.
public func bestEffortEquals(_ comparing: TLSConfiguration) -> Bool {
let isKeyLoggerCallbacksEqual = withUnsafeBytes(of: self.keyLogCallback) { callbackPointer1 in
Copy link
Member

Choose a reason for hiding this comment

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

given SR-14520 we should probably make this

let isBitEqual = withUnsafeBytes(of: self) { selfBits in
    withUnsafeBytes(of: comparing) { otherBits in
        return selfBits.elementsEqual(otherBits)
    }
}

with a comment liking the bug.

Copy link
Member

Choose a reason for hiding this comment

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

@Lukasa wdyt?

Copy link
Contributor

Choose a reason for hiding this comment

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

I still think we should stick with the closure-based version, but yes agree we should link the bug.

Copy link
Member

Choose a reason for hiding this comment

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

@Lukasa the problem is that if we do hit the bug, then they will never be equal. The allocations are for reabstraction thunks which aren't equal because they capture (the heap allocs) :P

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.

Thank you! That looks good to me!

@weissi
Copy link
Member

weissi commented May 3, 2021

@swift-nio-bot add to allowlist

Copy link
Contributor

@Lukasa Lukasa left a comment

Choose a reason for hiding this comment

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

Can you run the generate_linux_tests.rb script? That's part of the issues we're seeing in CI: we're missing your new tests on Linux.

@madsodgaard
Copy link
Contributor Author

@Lukasa Done!

@weissi weissi requested a review from Lukasa May 4, 2021 20:26
@weissi
Copy link
Member

weissi commented May 5, 2021

@Lukasa this is now all green, the failure is the new 5.5 job which doesn't have docker images yet ;)

@Lukasa Lukasa merged commit b2f8f35 into apple:main May 5, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🆕 semver/minor Adds new public API.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants