Skip to content

Inconsistent SO_NOSIGPIPE error handling across backends #230

@mvandeberg

Description

@mvandeberg

Summary

The SO_NOSIGPIPE setsockopt call in open_socket() is handled differently across backends. The kqueue backend treats failures as fatal (closes the fd, returns an error), while the select and epoll backends ignore the return value entirely. This should be made consistent.

Background

On macOS/FreeBSD, MSG_NOSIGNAL is not available for sendmsg(). To prevent SIGPIPE from killing the process when writing to a closed peer, each socket must have SO_NOSIGPIPE set at creation time. Linux uses MSG_NOSIGNAL on each sendmsg() call instead, so epoll services don't need SO_NOSIGPIPE at all.

Current behavior

Kqueue (macOS/FreeBSD) -- strict

All kqueue services check the setsockopt return and fail the open_socket / accept path if it returns -1:

  • kqueue_tcp_service.hpp -- checks, closes fd, returns error
  • kqueue_udp_service.hpp -- checks, closes fd, returns error
  • kqueue_local_stream_service.hpp -- checks, closes fd, returns error
  • kqueue_local_datagram_service.hpp -- checks (with #ifdef guard), closes fd, returns error
  • kqueue_local_stream_acceptor_service.hpp -- checks (with #ifdef guard) in accept path
  • kqueue_tcp_acceptor_service.hpp -- checks in accept path

Select (all POSIX) -- fire-and-forget

All select services call setsockopt but ignore the return value:

  • select_tcp_service.hpp -- ignores return
  • select_udp_service.hpp -- ignores return
  • select_local_stream_service.hpp -- ignores return
  • select_local_datagram_service.hpp -- ignores return
  • select_tcp_acceptor_service.hpp -- ignores return (in accept path)
  • select_local_stream_acceptor_service.hpp -- not yet added (may need it)

Epoll (Linux) -- not applicable

Epoll services don't use SO_NOSIGPIPE because Linux has MSG_NOSIGNAL. The epoll write policy passes MSG_NOSIGNAL to sendmsg() on every write.

Boost.Asio precedent

Asio treats SO_NOSIGPIPE failure as fatal in both socket creation and accept paths (asio/detail/impl/socket_ops.ipp):

  • Socket creation (line ~1900): after ::socket(), calls setsockopt(SO_NOSIGPIPE). On failure, closes the socket and returns invalid_socket with the error code.
  • Accept (line ~128): after ::accept(), calls setsockopt(SO_NOSIGPIPE) on the accepted fd. On failure, closes the accepted fd and returns invalid_socket.

Both are guarded by #if defined(__MACH__) && defined(__APPLE__) || defined(__FreeBSD__) -- a platform check rather than a feature check (#ifdef SO_NOSIGPIPE). This means Asio doesn't attempt SO_NOSIGPIPE on other BSDs (OpenBSD, NetBSD, DragonFly) even though they also define it.

The kqueue backend's strict error handling matches Asio's behavior. The select backend's fire-and-forget approach does not.

Questions to resolve

  1. Should SO_NOSIGPIPE failure be fatal? On any real BSD/macOS system, SO_NOSIGPIPE is always available and setsockopt should never fail on a freshly created socket. A failure here would indicate something deeply wrong (bad fd, kernel bug). The kqueue approach (fatal) matches Asio and seems more correct.

  2. Should select mirror kqueue? The select backend compiles on macOS too (BOOST_COROSIO_HAS_SELECT is !defined(_WIN32)). On macOS, a user could use the select backend and hit SIGPIPE if SO_NOSIGPIPE silently failed. Making select match kqueue's strict checking would be safer.

  3. Should the select acceptor services also set SO_NOSIGPIPE on accepted sockets? The kqueue acceptor services and select_tcp_acceptor_service do, but select_local_stream_acceptor_service does not. This is a gap.

  4. Should the #ifdef SO_NOSIGPIPE guard style be unified? Kqueue services use it inconsistently -- the original TCP/UDP services don't guard, while the newer local socket services do. Since kqueue only compiles on platforms that define SO_NOSIGPIPE, the guard is technically unnecessary there but is good hygiene for the select backend which compiles everywhere.

Proposed resolution

  1. Add #ifdef SO_NOSIGPIPE guards consistently to all setsockopt calls (already done for local socket services, needed for TCP/UDP).
  2. Check the return value in all backends (change select from fire-and-forget to fail-on-error, matching kqueue).
  3. Add SO_NOSIGPIPE to select_local_stream_acceptor_service accept path.
  4. Audit that every open_socket and accept path that creates a socket on a !MSG_NOSIGNAL platform sets SO_NOSIGPIPE.

Files to audit

include/boost/corosio/native/detail/kqueue/kqueue_tcp_service.hpp
include/boost/corosio/native/detail/kqueue/kqueue_udp_service.hpp
include/boost/corosio/native/detail/kqueue/kqueue_local_stream_service.hpp
include/boost/corosio/native/detail/kqueue/kqueue_local_datagram_service.hpp
include/boost/corosio/native/detail/kqueue/kqueue_tcp_acceptor_service.hpp
include/boost/corosio/native/detail/kqueue/kqueue_local_stream_acceptor_service.hpp
include/boost/corosio/native/detail/select/select_tcp_service.hpp
include/boost/corosio/native/detail/select/select_udp_service.hpp
include/boost/corosio/native/detail/select/select_local_stream_service.hpp
include/boost/corosio/native/detail/select/select_local_datagram_service.hpp
include/boost/corosio/native/detail/select/select_tcp_acceptor_service.hpp
include/boost/corosio/native/detail/select/select_local_stream_acceptor_service.hpp

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions