Skip to content

Harden ShareProxy: validate ports, avoid blocking the main actor, fix a shutdown race - #2

Open
AadhilFarhan wants to merge 2 commits into
mainfrom
fix/share-proxy-safety
Open

Harden ShareProxy: validate ports, avoid blocking the main actor, fix a shutdown race#2
AadhilFarhan wants to merge 2 commits into
mainfrom
fix/share-proxy-safety

Conversation

@AadhilFarhan

Copy link
Copy Markdown
Owner

What

Three related safety gaps in the LAN-sharing relay, bundled since they're all "make ShareProxy's lifecycle safe":

  1. Crash on invalid port. NWEndpoint.Port(rawValue: UInt16(targetPort))! trapped on any port outside 0...65535. Reachable today via Portside --share 99999 with no prior validation. init now validates and throws ShareProxyError.invalidPort instead.
  2. Main-thread stall. ShareProxy.init blocks briefly (up to 2s) waiting for its listener to come up, via a semaphore wait. AppModel.startSharing called it synchronously on the main actor, straight from QRPanel.onAppear — so tapping the QR button could freeze the UI, the same class of bug the project already guards against for PortScanner.scan(). startSharing is now async and dispatches construction the same way PortScanner.scan() already is, with a re-check of shares[port] after resuming in case another call raced it while suspended.
  3. Shutdown race. A connection already progressing through relay() when stop() runs could still land in the connections dict after stop() had already drained it — leaking a relay that's never cancelled. relay() now checks a stopped flag under the same lock stop() uses. Added deinit { stop() } as a safety net for callers that drop the last reference without calling stop().

Test plan

  • New ShareProxyTests: port 0, negative, and 70000 all throw ShareProxyError.invalidPort without touching the network (validation runs before the listener is ever created, so these are fast and non-flaky).
  • Ran the actual CLI end-to-end: Portside --share 99999 now prints a clean error and exits 1 instead of crashing.
  • Ran the actual relay end-to-end: started a real HTTP server on loopback, relayed it via Portside --share, curled the LAN address — HTTP 200, confirming the async/dispatch change didn't break the legitimate path.
  • Full suite: 12/12 passing.

… a shutdown race

- ShareProxy.init force-unwrapped an NWEndpoint.Port built from an
  unvalidated Int, trapping on any out-of-range port. Now validates
  1...65535 and throws ShareProxyError instead, matching the project's
  degrade-don't-crash convention. Reachable today via the CLI's
  --share flag with no prior validation.
- ShareProxy.init can block its calling thread briefly waiting for the
  listener to come up. AppModel.startSharing called it synchronously
  on the main actor from QRPanel's onAppear, so tapping the QR button
  could freeze the UI. startSharing is now async and dispatches the
  construction the same way PortScanner.scan() already is; re-checks
  shares[port] after resuming in case another call raced it.
- stop() could lose a race with an in-flight accept: a connection
  already progressing through relay() when cancel() ran could still
  land in the connections dict after stop() had already drained it,
  leaking an un-cancelled relay. relay() now checks a stopped flag
  under the same lock stop() uses.
- Added deinit { stop() } as a safety net if a caller drops the last
  reference without calling stop() explicitly.
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.

1 participant