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
-
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.
-
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.
-
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.
-
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
- Add
#ifdef SO_NOSIGPIPE guards consistently to all setsockopt calls (already done for local socket services, needed for TCP/UDP).
- Check the return value in all backends (change select from fire-and-forget to fail-on-error, matching kqueue).
- Add
SO_NOSIGPIPE to select_local_stream_acceptor_service accept path.
- 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
Summary
The
SO_NOSIGPIPEsetsockopt call inopen_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_NOSIGNALis not available forsendmsg(). To preventSIGPIPEfrom killing the process when writing to a closed peer, each socket must haveSO_NOSIGPIPEset at creation time. Linux usesMSG_NOSIGNALon eachsendmsg()call instead, so epoll services don't needSO_NOSIGPIPEat all.Current behavior
Kqueue (macOS/FreeBSD) -- strict
All kqueue services check the
setsockoptreturn and fail theopen_socket/ accept path if it returns -1:kqueue_tcp_service.hpp-- checks, closes fd, returns errorkqueue_udp_service.hpp-- checks, closes fd, returns errorkqueue_local_stream_service.hpp-- checks, closes fd, returns errorkqueue_local_datagram_service.hpp-- checks (with#ifdefguard), closes fd, returns errorkqueue_local_stream_acceptor_service.hpp-- checks (with#ifdefguard) in accept pathkqueue_tcp_acceptor_service.hpp-- checks in accept pathSelect (all POSIX) -- fire-and-forget
All select services call
setsockoptbut ignore the return value:select_tcp_service.hpp-- ignores returnselect_udp_service.hpp-- ignores returnselect_local_stream_service.hpp-- ignores returnselect_local_datagram_service.hpp-- ignores returnselect_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_NOSIGPIPEbecause Linux hasMSG_NOSIGNAL. The epoll write policy passesMSG_NOSIGNALtosendmsg()on every write.Boost.Asio precedent
Asio treats
SO_NOSIGPIPEfailure as fatal in both socket creation and accept paths (asio/detail/impl/socket_ops.ipp):::socket(), callssetsockopt(SO_NOSIGPIPE). On failure, closes the socket and returnsinvalid_socketwith the error code.::accept(), callssetsockopt(SO_NOSIGPIPE)on the accepted fd. On failure, closes the accepted fd and returnsinvalid_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 attemptSO_NOSIGPIPEon 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
Should
SO_NOSIGPIPEfailure be fatal? On any real BSD/macOS system,SO_NOSIGPIPEis always available andsetsockoptshould 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.Should select mirror kqueue? The select backend compiles on macOS too (
BOOST_COROSIO_HAS_SELECTis!defined(_WIN32)). On macOS, a user could use the select backend and hitSIGPIPEifSO_NOSIGPIPEsilently failed. Making select match kqueue's strict checking would be safer.Should the select acceptor services also set
SO_NOSIGPIPEon accepted sockets? The kqueue acceptor services andselect_tcp_acceptor_servicedo, butselect_local_stream_acceptor_servicedoes not. This is a gap.Should the
#ifdef SO_NOSIGPIPEguard 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 defineSO_NOSIGPIPE, the guard is technically unnecessary there but is good hygiene for the select backend which compiles everywhere.Proposed resolution
#ifdef SO_NOSIGPIPEguards consistently to all setsockopt calls (already done for local socket services, needed for TCP/UDP).SO_NOSIGPIPEtoselect_local_stream_acceptor_serviceaccept path.open_socketand accept path that creates a socket on a!MSG_NOSIGNALplatform setsSO_NOSIGPIPE.Files to audit