Skip to content

Fix kqueue close: clear SO_LINGER instead of calling shutdown#150

Merged
mvandeberg merged 1 commit intocppalliance:developfrom
mvandeberg:pr/fix-kqueue-close
Feb 17, 2026
Merged

Fix kqueue close: clear SO_LINGER instead of calling shutdown#150
mvandeberg merged 1 commit intocppalliance:developfrom
mvandeberg:pr/fix-kqueue-close

Conversation

@mvandeberg
Copy link
Contributor

@mvandeberg mvandeberg commented Feb 17, 2026

On macOS, close() with SO_LINGER {1,0} sends RST instead of FIN. RST does not reliably trigger EV_EOF on the peer's kqueue, causing pending reads to hang. The previous workaround called shutdown(SHUT_WR) before close to force a FIN.

Replace this with the approach used by Asio's kqueue reactor: clear SO_LINGER before close so the kernel sends FIN naturally. Also skip the explicit EV_DELETE (deregister_descriptor) on both sockets and acceptors, relying on kqueue's automatic cleanup when the fd is closed — matching Asio's deregister_descriptor(closing=true) path.

Update socket throughput benchmark to match asio.

Summary by CodeRabbit

Bug Fixes

  • Improved socket closure and lifecycle behavior to correctly handle the linger socket option during shutdown and destruction operations. The system now properly tracks and manages user-configured linger settings, ensuring appropriate cleanup of socket states and better resource management across all connection operations.

On macOS, close() with SO_LINGER {1,0} sends RST instead of FIN.
RST does not reliably trigger EV_EOF on the peer's kqueue, causing
pending reads to hang. The previous workaround called shutdown(SHUT_WR)
before close to force a FIN.

Replace this with the approach used by Asio's kqueue reactor: clear
SO_LINGER before close so the kernel sends FIN naturally. Also skip
the explicit EV_DELETE (deregister_descriptor) on both sockets and
acceptors, relying on kqueue's automatic cleanup when the fd is
closed — matching Asio's deregister_descriptor(closing=true) path.

Update socket throughput benchmark to match asio.
@coderabbitai
Copy link

coderabbitai bot commented Feb 17, 2026

📝 Walkthrough

Walkthrough

These changes modify the kqueue socket lifecycle to defer linger option cleanup and descriptor deregistration operations from the close_socket() method to shutdown() and destroy() paths, while introducing a private state flag to track user-set linger options.

Changes

Cohort / File(s) Summary
kqueue acceptor close logic
include/boost/corosio/native/detail/kqueue/kqueue_acceptor_service.hpp
Removed file descriptor deregistration call from close_socket(); now only closes fd and resets state without touching scheduler registration.
kqueue socket linger tracking
include/boost/corosio/native/detail/kqueue/kqueue_socket.hpp
Added private boolean member user_set_linger_ initialized to false to track whether SO_LINGER was explicitly set by user code.
kqueue socket service lifecycle management
include/boost/corosio/native/detail/kqueue/kqueue_socket_service.hpp
Modified socket lifecycle: set_linger() marks the flag when SO_LINGER is applied; close_socket() removes proactive shutdown/deregister steps and clears the flag; shutdown() and destroy() now conditionally clear SO_LINGER based on the flag before closing sockets.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 A socket's linger now takes pause,
No hasty close without a cause,
Defer the cleanup, mark the way,
Till shutdown's touch ends the day! ✨

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: replacing shutdown() calls with SO_LINGER clearing in the kqueue close path, which is the primary objective of this PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cppalliance-bot
Copy link

An automated preview of the documentation is available at https://150.corosio.prtest3.cppalliance.org/index.html

If more commits are pushed to the pull request, the docs will rebuild at the same URL.

2026-02-17 21:42:00 UTC

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
include/boost/corosio/native/detail/kqueue/kqueue_socket_service.hpp (1)

996-1000: ⚠️ Potential issue | 🟠 Major

close() path does not clear SO_LINGER — RST may still be sent on user-initiated close.

The destroy() and shutdown() paths both clear SO_LINGER before closing the fd, but close() delegates directly to close_socket() which just closes the fd without clearing linger. Since fd_ is set to -1 in close_socket(), the subsequent destroy() call can no longer reach the fd to clear linger (guard on line 923 fails).

If a user sets SO_LINGER{1,0} and then explicitly closes the socket, the kernel will still send RST instead of FIN on macOS — the exact scenario this PR aims to fix.

Proposed fix — clear linger in close() before delegating
 inline void
 kqueue_socket_service::close(io_object::handle& h)
 {
-    static_cast<kqueue_socket*>(h.get())->close_socket();
+    auto* kq_impl = static_cast<kqueue_socket*>(h.get());
+    if (kq_impl->user_set_linger_ && kq_impl->fd_ >= 0)
+    {
+        struct ::linger lg;
+        lg.l_onoff  = 0;
+        lg.l_linger = 0;
+        ::setsockopt(kq_impl->fd_, SOL_SOCKET, SO_LINGER, &lg, sizeof(lg));
+    }
+    kq_impl->close_socket();
 }

Alternatively, consider extracting the linger-clearing logic into a small helper to avoid the now three-way duplication (see next comment).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@include/boost/corosio/native/detail/kqueue/kqueue_socket_service.hpp` around
lines 996 - 1000, The close path currently calls kqueue_socket::close_socket()
directly and therefore doesn't clear SO_LINGER before the fd is closed (unlike
destroy() and shutdown()), which allows an RST to be sent; modify
kqueue_socket_service::close to clear the socket's SO_LINGER option on the
underlying fd (the same linger-clearing logic used in destroy()/shutdown())
before delegating to kqueue_socket::close_socket(), or factor that
linger-clearing code into a small helper (e.g., clear_socket_linger(fd) used by
destroy(), shutdown(), and close()) so all three paths clear SO_LINGER prior to
closing.
🧹 Nitpick comments (1)
include/boost/corosio/native/detail/kqueue/kqueue_socket_service.hpp (1)

874-898: Consider extracting a clear_linger_if_needed helper to reduce duplication.

The same linger-clearing pattern (check user_set_linger_ + fd_ >= 0, then setsockopt) is repeated in shutdown() and destroy(), and should also appear in close() (see previous comment). A small private helper on kqueue_socket would consolidate the logic:

Example helper
// In kqueue_socket:
void clear_user_linger() noexcept
{
    if (user_set_linger_ && fd_ >= 0)
    {
        struct ::linger lg;
        lg.l_onoff  = 0;
        lg.l_linger = 0;
        ::setsockopt(fd_, SOL_SOCKET, SO_LINGER, &lg, sizeof(lg));
    }
}

Then shutdown(), destroy(), and close() each call impl->clear_user_linger() before close_socket().

Also applies to: 915-935

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@include/boost/corosio/native/detail/kqueue/kqueue_socket_service.hpp` around
lines 874 - 898, Extract the repeated linger-clearing logic into a private
method on kqueue_socket (e.g. kqueue_socket::clear_user_linger() noexcept) that
checks user_set_linger_ and fd_ >= 0 and calls ::setsockopt(fd_, SOL_SOCKET,
SO_LINGER, &lg, sizeof(lg)) with l_onoff=0,l_linger=0; then replace the
duplicated block in kqueue_socket_service::shutdown() and the similar blocks in
kqueue_socket_service::destroy() and kqueue_socket::close() to simply call
impl->clear_user_linger() before impl->close_socket(); keep the helper noexcept
and name it so it clearly indicates it clears the user-set linger option.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@include/boost/corosio/native/detail/kqueue/kqueue_socket_service.hpp`:
- Around line 996-1000: The close path currently calls
kqueue_socket::close_socket() directly and therefore doesn't clear SO_LINGER
before the fd is closed (unlike destroy() and shutdown()), which allows an RST
to be sent; modify kqueue_socket_service::close to clear the socket's SO_LINGER
option on the underlying fd (the same linger-clearing logic used in
destroy()/shutdown()) before delegating to kqueue_socket::close_socket(), or
factor that linger-clearing code into a small helper (e.g.,
clear_socket_linger(fd) used by destroy(), shutdown(), and close()) so all three
paths clear SO_LINGER prior to closing.

---

Nitpick comments:
In `@include/boost/corosio/native/detail/kqueue/kqueue_socket_service.hpp`:
- Around line 874-898: Extract the repeated linger-clearing logic into a private
method on kqueue_socket (e.g. kqueue_socket::clear_user_linger() noexcept) that
checks user_set_linger_ and fd_ >= 0 and calls ::setsockopt(fd_, SOL_SOCKET,
SO_LINGER, &lg, sizeof(lg)) with l_onoff=0,l_linger=0; then replace the
duplicated block in kqueue_socket_service::shutdown() and the similar blocks in
kqueue_socket_service::destroy() and kqueue_socket::close() to simply call
impl->clear_user_linger() before impl->close_socket(); keep the helper noexcept
and name it so it clearly indicates it clears the user-set linger option.

@cppalliance-bot
Copy link

GCOVR code coverage report https://150.corosio.prtest3.cppalliance.org/gcovr/index.html
LCOV code coverage report https://150.corosio.prtest3.cppalliance.org/genhtml/index.html
Coverage Diff Report https://150.corosio.prtest3.cppalliance.org/diff-report/index.html

Build time: 2026-02-17 21:44:48 UTC

@codecov
Copy link

codecov bot commented Feb 17, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 82.26%. Comparing base (3d80389) to head (dff869c).
⚠️ Report is 3 commits behind head on develop.

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff            @@
##           develop     #150   +/-   ##
========================================
  Coverage    82.26%   82.26%           
========================================
  Files           70       70           
  Lines         5876     5876           
========================================
  Hits          4834     4834           
  Misses        1042     1042           

Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 3d80389...dff869c. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@mvandeberg mvandeberg merged commit dd86e28 into cppalliance:develop Feb 17, 2026
30 of 31 checks passed
@mvandeberg mvandeberg deleted the pr/fix-kqueue-close branch February 17, 2026 22:10
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

Comments