Skip to content

Request an HTTP upgrade when attaching to stdin - #610

Open
tas50 wants to merge 1 commit into
upserve:masterfrom
tas50:fix-attach-stdin-upgrade
Open

Request an HTTP upgrade when attaching to stdin#610
tas50 wants to merge 1 commit into
upserve:masterfrom
tas50:fix-attach-stdin-upgrade

Conversation

@tas50

@tas50 tas50 commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Container#attach(stdin:) hijacks the socket, but never asked the daemon to upgrade the connection. Docker then answers 200 and leaves the payload on Excon's buffered socket — which the raw hijacked read never looks at. The attach blocks forever, with no timeout and no way to interrupt it:

hijack: hijacking the HTTP socket
hijack: starting stdin copy thread
hijack: copying stdin => socket
hijack: closing write end of hijacked socket
hijack: reading from hijacked socket
<hangs indefinitely>

Why this is the gem, not the platform

A raw socket performing the identical exchange with the upgrade headers works perfectly:

wrote + close_write
GOT "\x01\x00\x00\x00\x00\x00\x00\bfoo\nbar\n"
EOF (clean close)

The fix

Send Connection: Upgrade / Upgrade: tcp on the hijacked request. Docker then replies 101 Switching Protocols and hands the socket over — which also means 101 has to be an accepted status, since the default expects list is 200..204 plus 301/304 and was otherwise rejecting the reply:

Expected([200, 201, 202, 203, 204, 301, 304]) <=> Actual(101 Switching Protocols)

Verification

The spec that used to hang indefinitely now passes:

Docker::Container#attach with stdin
  yields the output          # exit=0, previously killed at timeout

And container_spec can now be run to completion on Docker Desktop for the first time — 60 examples, 2 failures, both confirmed pre-existing on master and both macOS-only (they pass on the Linux runners):

  • #exec when stdin object is passedDocker::Exec#start! hijacks the same way and has the same defect
  • #export yields each chunk — unrelated

Two specs assert the contract; both were confirmed failing before the change.

Scope

Only the stdin branch is touched — attach without stdin is unchanged, and its existing specs still pass. Docker::Exec#start! has the identical defect and is left for a follow-up so this stays reviewable.

CI will be red until #605 lands.

Container#attach(stdin:) hijacks the socket, but never asked the daemon to
upgrade the connection. Docker then answers 200 and leaves the payload on
Excon's buffered socket, which the raw hijacked read never looks at, so the
attach blocks forever with no timeout and no way to interrupt it:

    hijack: copying stdin => socket
    hijack: closing write end of hijacked socket
    hijack: reading from hijacked socket
    <hangs indefinitely>

A raw socket performing the same exchange with the upgrade headers works
correctly, so this is the request, not the platform. Sending them makes Docker
reply 101 and hand the socket over, which also means 101 has to be an accepted
status - the default expects list is 200..204 plus 301 and 304, so the reply
was otherwise rejected.

The spec that used to hang now passes, and container_spec can be run to
completion on Docker Desktop for the first time.

Only the stdin branch is touched; attach without stdin is unchanged.

Docker::Exec#start! hijacks the same way and has the same defect. It is left
for a follow-up so this stays reviewable.
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