Skip to content

Fix indefinite parent hang when child dies mid-handshake - #5391

Closed
nebojsaj1726 wants to merge 1 commit into
opencontainers:mainfrom
nebojsaj1726:5087-sync-socket-hang
Closed

nebojsaj1726 wants to merge 1 commit into
opencontainers:mainfrom
nebojsaj1726:5087-sync-socket-hang

Conversation

@nebojsaj1726

@nebojsaj1726 nebojsaj1726 commented Aug 10, 2026 •

Copy link
Copy Markdown

Fixes #5087.

When the container's init process is killed by a seccomp filter mid-handshake, only the thread that made the offending syscall is terminated (SCMP_ACT_KILL / SECCOMP_RET_KILL_THREAD kills a single thread, not the whole process - see seccomp(2)). Since runc init is a Go binary, other runtime threads (GC, sysmon, etc.) keep running regardless, so the process can never fully exit - the kernel never delivers EOF on the sync socket, and the parent blocks forever in recvfrom().

This adds waitForSyncReady, which polls the sync socket alongside the child's own /proc state before each read in parseSync, so the parent can detect the child's death directly (via Zombie state) rather than relying on the socket's own EOF behavior - the same approach waitForFifoReadyPolling already uses for the exec-fifo handoff. If the child is confirmed dead without its socket ever becoming genuinely ready, parseSync now returns a clear error instead of hanging or silently reporting success.

Verified two ways:

  • A new test (TestParseSyncDeadChildDoesNotHang) exercises the real parseSync against a socket with a deliberately leaked duplicate fd and a real killed process - this simulates the same externally observable symptom (no EOF ever arrives) as the real seccomp scenario above, without needing an actual seccomp filter in the test itself. Fails (5s timeout) on unpatched main, passes (~0.3s) with this fix.
  • Manual repro following the original issue's steps (seccomp filter killing write, runc run): hangs indefinitely on unpatched main; returns immediately with a clear error on this branch.

What's not yet done / open questions for maintainers:

  • I initially tried a pidfd-based fast path (mirroring waitForFifoReadyPidfd/waitForFifoReadyPolling in container_linux.go), which passed in isolated unit tests but hung unpredictably during real container startup. Given the thread-kill mechanism above, I suspect a bare poll() on the pidfd doesn't reliably fire when only the thread-group leader is killed while sibling threads survive - this may also affect the already-merged waitForFifoReadyPidfd in #5271. This draft ships polling-only for now; can dig further into the pidfd path if that's wanted.
  • Only the two parseSync call sites (initProcess/setnsProcess) are covered. I didn't find other parent-side callers with the same exposure, but would appreciate your insight.
  • Test coverage is currently one test exercising parseSync directly; it isn't wired through an actual runc run/exec integration test.
  • No CHANGELOG entry yet - wanted to confirm the direction is right first.

Opening as a draft to get feedback on the approach before finishing this up.

If the container's init process dies mid-handshake (e.g. killed
by a seccomp filter) while some other reference to its end of the
sync socket is still held open elsewhere, the parent can block
forever in recvfrom(), since the kernel never delivers EOF on a
socket with a live reference.

Add waitForSyncReady, which polls the sync socket alongside the
child's own /proc state before each read in parseSync, so the
parent can detect the child's death directly instead of relying on
the socket's own EOF behavior. If the child is confirmed dead
without its socket ever becoming ready, parseSync now returns a
clear error instead of hanging or silently reporting success.

Fixes opencontainers#5087

Signed-off-by: Nebojsa Jacovic <nebojsa.jacovic@gmail.com>
@kolyshkin

Copy link
Copy Markdown
Contributor

The description and the patch is somewhat misleading. What happens (I believe) is this: #5087 (comment)

@nebojsaj1726

Copy link
Copy Markdown
Author

Thanks - I've read issue comment and now it's clearer. That's a different mechanism than what I described (leaked fd reference) - I'll update the description.

So, the fix itself doesn't need to be changed - waitForSyncReady already checks for Zombie state via system.Stat, same as waitForFifoReadyPolling, and both the test and manual repro confirm it resolves the hang.

Please let me know if I'm missing something.

@kolyshkin

Copy link
Copy Markdown
Contributor

Closing in favor of #5431, thank you @nebojsaj1726

@kolyshkin kolyshkin closed this Sep 3, 2026
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.

Indefinite parent hang in ReadPacket upon abnormal child exit after procRun

2 participants