maxgoedjen / secretive

Store SSH keys in the Secure Enclave

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Agent closes connection on short read

kdrag0n opened this issue · comments

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

guard data.count > 4 else { return false}

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:

  • Make it stateful, and buffer data in a per-connection state object
  • Use Swift concurrency instead of notification-based async I/O
  • Spawn a thread per connection, and use blocking I/O on each connection instead. (The listener can still be async.)

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: orbstack/orbstack#625

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

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

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.

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.