Skip to content

Fix SSH2 resource ownership and stream error handling#6113

Open
binaryfire wants to merge 6 commits into
swoole:masterfrom
binaryfire:fix/ssh2-resource-stream-lifecycle
Open

Fix SSH2 resource ownership and stream error handling#6113
binaryfire wants to merge 6 commits into
swoole:masterfrom
binaryfire:fix/ssh2-resource-stream-lifecycle

Conversation

@binaryfire

@binaryfire binaryfire commented Jul 21, 2026

Copy link
Copy Markdown

Problem

The bundled SSH2 extension has one connected set of resource-lifetime and stream-contract bugs:

  • direct SFTP wrapper connections do not consistently register and release their session and SFTP resources;
  • one-shot wrapper operations leak temporary references;
  • child resources can call libssh2 after their parent session has been destroyed;
  • channel reads, writes, EOF, blocking mode, and timeouts do not follow PHP stream semantics; and
  • SFTP file and directory errors can be reported as clean EOF.

These paths share the same ownership graph and stream implementations. Splitting them across pull requests would leave unsafe intermediate states.

Change

The series contains five implementation commits plus one test-isolation follow-up:

  1. Initialize every field on SFTP directory handles before exposing the stream.
  2. Make direct wrapper session/SFTP ownership transactional and release temporary wrapper resources.
  3. Guard child operations and destructors with their parent resource lifetime.
  4. Correct channel blocking, nonblocking backpressure, operation-local timeouts, error returns, and EOF metadata.
  5. Keep SFTP file and directory failures distinct from clean completion.
  6. Keep the authentication-failure regression below OpenSSH 9.8+'s default per-source penalty floor without weakening its descriptor-leak assertion.

Channel timeouts are passed directly to the owning socket poll. They no longer mutate shared session socket state, so a timeout on one channel cannot affect another channel or a later SFTP operation on the same SSH session.

Tests

New PHPT coverage exercises:

  • SFTP directory enumeration;
  • direct wrapper authentication, resource registration, and reference release;
  • child-resource behavior after parent-session closure;
  • channel blocking mode, nonblocking backpressure, transport errors, EOF, and sub-second timeout isolation; and
  • SFTP file and directory error handling.

Each implementation commit was built and tested at its own boundary. The complete added SSH2 test set also passes against the final extension build.

Review notes

The affected ownership paths were compared with PECL ssh2 1.5.0. This keeps the established Zend resource model where it applies, while retaining Swoole's coroutine-aware I/O and adding the Swoole-specific channel timeout and nonblocking behavior.

The parent-guard commit also changes (data->refcount)-- to (*(data->refcount))-- in the allocation-failure cleanup for ssh2_fetch_stream(). That branch cannot be forced through a normal PHPT without a production test hook, but the current expression advances the pointer instead of decrementing the referenced count.

Initialize the session and SFTP pointers on directory stream handles before the stream is exposed. This gives directory reads and cleanup the same complete handle state as ordinary SFTP file streams.

Add a PHPT that creates a remote directory, enumerates its entries through the SFTP wrapper, and removes the fixture.
Register direct wrapper sessions before authentication so every failure path releases an initialized Zend resource. Register the SFTP subsystem against that owned session and transfer the existing references to successful wrapper streams.

Release temporary SFTP resources after one-shot stat and mutation operations. Add PHPT coverage for authentication failure, direct subsystem registration, and repeated wrapper operations.
Introduce a shared parent-session predicate and apply it across channel, listener, public-key, and SFTP operations. Child resources now return their normal PHP failure values after the owning session is closed while destructors still release Zend references and local bookkeeping.

Correct the fetched-stream refcount decrement and cover the resource graph with a regression that disconnects parent sessions before exercising and closing their children.
Initialize channels as blocking PHP streams and let explicit nonblocking operations expose libssh2 backpressure without entering the coroutine-yielding hook. Preserve negative transport errors, derive EOF only from zero-byte reads, and publish channel-local stream metadata.

Pass per-stream timeouts directly to socket polling so sub-second precision is retained and one channel cannot alter the shared session socket for later operations. Cover blocking defaults, nonblocking EAGAIN, stream errors, EOF transitions, timeout metadata, and timeout isolation.
Keep negative SFTP file reads as errors instead of marking them as EOF. For directory streams, make each native result own the EOF state so clean completion, transport failure, and a closed parent session remain distinguishable through PHP's stream API.

Add regressions for normal file and directory completion as well as failed reads after the parent SSH session is disconnected.
Limit the descriptor-leak regression to one warmup failure and one observed failure. This remains sufficient to detect the leaked session descriptor while staying below OpenSSH 9.8+'s default per-source penalty floor.

Document the limit so later test expansion does not temporarily block unrelated tests sharing the SSH server.
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