Skip to content

Use MSG_DONTWAIT instead of fcntl with O_NONBLOCK for sockets in sending/receiving#1727

Open
Lkzeu wants to merge 1 commit intochriskohlhoff:masterfrom
Lkzeu:master
Open

Use MSG_DONTWAIT instead of fcntl with O_NONBLOCK for sockets in sending/receiving#1727
Lkzeu wants to merge 1 commit intochriskohlhoff:masterfrom
Lkzeu:master

Conversation

@Lkzeu
Copy link
Copy Markdown

@Lkzeu Lkzeu commented Mar 13, 2026

On Linux, FreeBSD, OpenBSD and other systems, it's possible to have a non-blocking behavior on sockets using the flag MSG_DONTWAIT on sending and receiving functions per syscall, without needing to set the socket in a non-blocking state globally using ioctl or fcntl. This change will save a few syscalls to achieve the same results.

Another issue with O_NONBLOCK appears when sandboxing. The O_NONBLOCK state is shared among all copies of a file descriptor¹ and therefore if a sandboxed process tries to disable the non-blocking state of a received/inherited socket in a loop, it could potentially freeze the thread of the parent process.

This change was tested and works on Linux and FreeBSD

¹ https://blog.emilua.org/2025/01/12/software-sandboxing-basics/#_non_blocking_io_on_unix_it_sucks

A minimal echo protocol program

#include <asio.hpp>
#include <iostream>
#include <memory>
#include <string_view>
#include <system_error>

using namespace asio::local;

int main()
{
  try
  {
    asio::io_context io_context(1);

    stream_protocol::socket s1(io_context);
    stream_protocol::socket s2(io_context);
    connect_pair(s1, s2);
    
    auto buffer = std::make_shared_for_overwrite<char[]>(128);
    
    s1.async_send(
      asio::buffer("ping"),
      [buffer, &s1](asio::error_code, std::size_t) {
        s1.async_read_some(asio::buffer(buffer.get(), 128),
          [buffer](asio::error_code ec, std::size_t nread) {
            if (ec)
              throw std::system_error(ec);
              
            std::string_view sv(buffer.get(), nread); 
            std::cout << sv << '\n';
          }
        );
      }
    );

    auto buffer2 = std::make_shared_for_overwrite<char[]>(128);
    
    s2.async_read_some(asio::buffer(buffer2.get(), 128),
      [buffer2, &s2](asio::error_code ec, std::size_t nread) {
        if (ec)
          throw std::system_error(ec);
      
        s2.async_write_some(asio::buffer(buffer2.get(), nread),
          asio::detached
        );
      }
    );

    io_context.run();
  }
  catch (std::exception& e)
  {
    std::printf("Exception: %s\n", e.what());
  }
}

By running strace on the echo program before and after this patch you can see that the two calls to fcntl (to get flags and to set the O_NONBLOCK) for each socket is not called anymore on the patched version.

--- before_patch.txt	2026-03-11 12:32:39.522959691 -0300
+++ after_patch.txt 	2026-03-11 12:48:49.532765016 -0300
@@ -69,17 +69,13 @@
 socketpair(AF_UNIX, SOCK_STREAM, 0, [6, 7]) = 0
 epoll_ctl(4, EPOLL_CTL_ADD, 6, {events=EPOLLIN|EPOLLPRI|EPOLLERR|EPOLLHUP|EPOLLET, data=0x55eb3d9b4340}) = 0
 epoll_ctl(4, EPOLL_CTL_ADD, 7, {events=EPOLLIN|EPOLLPRI|EPOLLERR|EPOLLHUP|EPOLLET, data=0x55eb3d9b43e0}) = 0
-fcntl(6, F_GETFL)                       = 0x2 (flags O_RDWR)
-fcntl(6, F_SETFL, O_RDWR|O_NONBLOCK)    = 0
-sendto(6, "ping\0", 5, MSG_NOSIGNAL, NULL, 0) = 5
-fcntl(7, F_GETFL)                       = 0x2 (flags O_RDWR)
-fcntl(7, F_SETFL, O_RDWR|O_NONBLOCK)    = 0
-recvfrom(7, "ping\0", 128, 0, NULL, NULL) = 5
+sendto(6, "ping\0", 5, MSG_DONTWAIT|MSG_NOSIGNAL, NULL, 0) = 5
+recvfrom(7, "ping\0", 128, MSG_DONTWAIT, NULL, NULL) = 5
 epoll_wait(4, [{events=EPOLLIN, data=0x55eb3d9b42dc}], 128, 0) = 1
-recvfrom(6, 0x55eb3d9b4480, 128, 0, NULL, NULL) = -1 EAGAIN (Resource temporarily unavailable)
-sendto(7, "ping\0", 5, MSG_NOSIGNAL, NULL, 0) = 5
+recvfrom(6, 0x55eb3d9b4480, 128, MSG_DONTWAIT, NULL, NULL) = -1 EAGAIN (Resource temporarily unavailable)
+sendto(7, "ping\0", 5, MSG_DONTWAIT|MSG_NOSIGNAL, NULL, 0) = 5
 epoll_wait(4, [{events=EPOLLIN, data=0x55eb3d9b4340}], 128, 0) = 1
-recvfrom(6, "ping\0", 128, 0, NULL, NULL) = 5
+recvfrom(6, "ping\0", 128, MSG_DONTWAIT, NULL, NULL) = 5
 fstat(1, {st_mode=S_IFCHR|0600, st_rdev=makedev(0x88, 0x1), ...}) = 0
 write(1, "ping\0\n", 6)                 = 6
 epoll_ctl(4, EPOLL_CTL_DEL, 7, 0x7ffec38432bc) = 0

…ing/receiving

On Linux, FreeBSD, OpenBSD and other systems, it's possible to have a
non-blocking behavior on sockets using the flag MSG_DONTWAIT on sending
and receiving functions per syscall, without needing to set the socket in
a non-blocking state globally using ioctl or fcntl.
This change will save a few syscalls to achieve the same results.
Copy link
Copy Markdown
Contributor

@vinipsmaker vinipsmaker left a comment

Choose a reason for hiding this comment

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

I reviewed the code and tested on FreeBSD with Capsicum enabled (so O_NONBLOCK wouldn't even work if it was being used when it shouldn't).

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.

2 participants