maxgoedjen / secretive

Store SSH keys in the Secure Enclave
MIT License
7.16k stars 157 forks source link

Agent closes connection on short read #483

Closed kdrag0n closed 9 months ago

kdrag0n commented 1 year ago

Upon receiving a notification that a connection socket is readable, the agent reads availableData and assumes that the data constitutes a full request message:

https://github.com/maxgoedjen/secretive/blob/0944d65ccb0d0b71a686995f7beeef4a8e9ad516/Sources/Packages/Sources/SecretAgentKit/Agent.swift#L41

However, since the OpenSSH client writes the 4-byte length and the message payload separately, there's a race condition where the agent could read only the 4-byte length header, then reject the message for being incomplete. In practice this almost never happens with a local Unix socket client, but it can happen when forwarding to a VM over TCP due to increased latency and TCP buffering semantics.

24552:2023-09-07 23:47:14.783745-0700 0x85fe73   Debug       0x0                  88408  0    SecretAgent: Socket controller has new data available
24553:2023-09-07 23:47:14.783844-0700 0x85fe73   Debug       0x0                  88408  0    SecretAgent: Socket controller received new file handle
24554:2023-09-07 23:47:14.783868-0700 0x85fe73   Debug       0x0                  88408  0    SecretAgent: [com.maxgoedjen.secretive.secretagent.agent:] Agent handling new data
24555:2023-09-07 23:47:14.784020-0700 0x85fe73   Debug       0x0                  88408  0    SecretAgent: Socket controller called with empty data, socked closed

I think this is a bit complicated to fix with the current design of the SocketController, since it's stateless, single-threaded, and async. Possible solutions that I can think of:

It's not safe to use the synchronous readDataUpToLength with the current design because a client could block the agent thread by not sending anything after the length header. An ideal solution would read the length into a 4-byte buffer, then wait for the payload asynchronously and continue reading until the full buffer has been filled or the connection has been closed.

This can be reproduced with OrbStack v0.17.1: https://github.com/orbstack/orbstack/issues/625

espadolini commented 11 months ago

Might be a dupe of #392 but with an explanation.

maxgoedjen commented 11 months ago

That would indeed explain #392 – thanks for the detailed explanation @kdrag0n and @espadolini for making that connection.

maxgoedjen commented 11 months ago

I've been mulling rewriting those bits in more modern async-await paradigms for a while but didn't have a great reason to do so, that might be a good enough one.

maxgoedjen commented 9 months ago

So this might be a little simpler than I thought – basically the issue is https://github.com/maxgoedjen/secretive/blob/main/Sources/SecretAgent/AppDelegate.swift#L34 blocks the main queue – this can actually (for the most part) be shunted off to a concurrent queue and handle two requests in parallel (tested by putting some synthetic hangs in there and running a few requests at once) – ie, the most of the existing socket code is capable of dealing with multiple connections at once. I'm still gonna play with cleaning this up a little and seeing if it's more manageable with modern concurrency.