Skip to content

Wait out socket backpressure when streaming fds across a clone - #297

Closed
doanbaotrung wants to merge 1 commit into
sysprog21:mainfrom
open-sources-port:socket-backpressure
Closed

Wait out socket backpressure when streaming fds across a clone#297
doanbaotrung wants to merge 1 commit into
sysprog21:mainfrom
open-sources-port:socket-backpressure

Conversation

@doanbaotrung

@doanbaotrung doanbaotrung commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator

fork_ipc_send_fds chunks descriptors at 120 per SCM_RIGHTS message, which bounds each control message but not how many sit unread in the socket at once. The parent streams every chunk in a tight loop while the freshly cloned child is still starting, so it outruns the receiver by a full socket buffer.

macOS refuses a control message that does not fit rather than queuing it, so a blocking sendmsg reports EMSGSIZE where a data-only write would block. At the default 8 KiB buffer that lands after about 1900 descriptors, which a guest reaches once its region list grows large enough -- dpkg passed it around the 198th package of an install:

clone: send backing fds failed: Message too long
clone: failed to send process state
dpkg: unrecoverable fatal error, aborting:
fork failed: Cannot allocate memory

Treat EMSGSIZE from a fixed-size chunk as backpressure: wait for writability and retry the same chunk. The child drains concurrently, and POLLOUT stays clear while the buffer holds control mbufs, so this blocks rather than spins. Waiting without a deadline matches fork_ipc_write_all on the same socket; a child that dies surfaces as POLLHUP.


Summary by cubic

Waits out socket backpressure when streaming SCM_RIGHTS fds during clone, preventing EMSGSIZE failures on macOS. Treats EMSGSIZE from a fixed-size chunk as backpressure: waits for POLLOUT and retries the same chunk. Also makes SO_NOSIGPIPE best-effort so the clone does not fail if the option cannot be applied.

  • Old vs new behavior: previously the parent sent all chunks in a tight loop and aborted the clone when sendmsg returned EMSGSIZE; now we block until the socket is writable and resend, matching fork_ipc_write_all semantics. Child death still surfaces as POLLHUP.
  • Review focus: fork_ipc_send_fds retry path on EMSGSIZE, and the removal of hard failure on setsockopt(SO_NOSIGPIPE); option is applied on both ends when available but does not gate clone success.

Written for commit 51c07dc. Summary will update on new commits.

Review in cubic

fork_ipc_send_fds chunks descriptors at 120 per SCM_RIGHTS message, which
bounds each control message but not how many sit unread in the socket at
once. The parent streams every chunk in a tight loop while the freshly
cloned child is still starting, so it outruns the receiver by a full
socket buffer.

macOS refuses a control message that does not fit rather than queuing it,
so a blocking sendmsg reports EMSGSIZE where a data-only write would
block. At the default 8 KiB buffer that lands after about 1900
descriptors, which a guest reaches once its region list grows large
enough -- dpkg passed it around the 198th package of an install:

  clone: send backing fds failed: Message too long
  clone: failed to send process state
  dpkg: unrecoverable fatal error, aborting:
   fork failed: Cannot allocate memory

Treat EMSGSIZE from a fixed-size chunk as backpressure: wait for
writability and retry the same chunk. The child drains concurrently, and
POLLOUT stays clear while the buffer holds control mbufs, so this blocks
rather than spins. Waiting without a deadline matches fork_ipc_write_all
on the same socket; a child that dies surfaces as POLLHUP.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/runtime/forkipc.c">

<violation number="1" location="src/runtime/forkipc.c:1554">
P1: If the parent endpoint's `setsockopt` fails, `sys_clone` continues with an unprotected `ipc_sock`; a child that exits during the handshake can then raise the host's default SIGPIPE and terminate elfuse instead of returning a clone error. Restore fail-closed handling for either option, or otherwise guarantee SIGPIPE suppression before sending.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/runtime/forkipc.c
Comment on lines +1554 to +1557
setsockopt(sock_fds[0], SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe,
sizeof(nosigpipe));
setsockopt(sock_fds[1], SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe,
sizeof(nosigpipe));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: If the parent endpoint's setsockopt fails, sys_clone continues with an unprotected ipc_sock; a child that exits during the handshake can then raise the host's default SIGPIPE and terminate elfuse instead of returning a clone error. Restore fail-closed handling for either option, or otherwise guarantee SIGPIPE suppression before sending.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/runtime/forkipc.c, line 1554:

<comment>If the parent endpoint's `setsockopt` fails, `sys_clone` continues with an unprotected `ipc_sock`; a child that exits during the handshake can then raise the host's default SIGPIPE and terminate elfuse instead of returning a clone error. Restore fail-closed handling for either option, or otherwise guarantee SIGPIPE suppression before sending.</comment>

<file context>
@@ -1549,21 +1549,12 @@ int64_t sys_clone(hv_vcpu_t vcpu,
-        close(sock_fds[1]);
-        return -LINUX_ENOMEM;
-    }
+    setsockopt(sock_fds[0], SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe,
+               sizeof(nosigpipe));
+    setsockopt(sock_fds[1], SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe,
</file context>
Suggested change
setsockopt(sock_fds[0], SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe,
sizeof(nosigpipe));
setsockopt(sock_fds[1], SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe,
sizeof(nosigpipe));
if (setsockopt(sock_fds[0], SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe,
sizeof(nosigpipe)) < 0 ||
setsockopt(sock_fds[1], SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe,
sizeof(nosigpipe)) < 0) {
log_error("clone: SO_NOSIGPIPE failed: %s", strerror(errno));
close(sock_fds[0]);
close(sock_fds[1]);
return -LINUX_ENOMEM;
}

@doanbaotrung
doanbaotrung marked this pull request as draft August 15, 2026 06:49
@doanbaotrung
doanbaotrung deleted the socket-backpressure branch August 15, 2026 06:50
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