Skip to content

Fix selector teardown leak from libev size-zero allocator calls - #336

Merged
ioquatix merged 1 commit into
socketry:mainfrom
nateberkopec:fix-libev-zero-size-allocator
Oct 9, 2026
Merged

ioquatix merged 1 commit into
socketry:mainfrom
nateberkopec:fix-libev-zero-size-allocator

Conversation

@nateberkopec

@nateberkopec nateberkopec commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

This is AI-assisted. Model: gpt-6.1-sol

Summary

Fixes #335.

libev's allocator contract treats realloc(ptr, 0) as "free this and return NULL", and libev discards the return value of those size-zero calls during teardown. nio4r currently passes Ruby's xrealloc directly to ev_set_allocator, but it does not implement that contract: both xrealloc(NULL, 0) and xrealloc(ptr, 0) return a new one-byte allocation (allocating the replacement before freeing the original pointer when non-NULL). libev throws that pointer away, so every selector teardown leaks a little memory.

This PR adds a small adapter in ext/nio4r/nio4r_ext.c:

  • size == 0: xfree(pointer) and return NULL, matching libev's expectation.
  • otherwise: xrealloc(pointer, size), so allocations still go through Ruby's allocator and GC malloc accounting.

The vendored libev sources are unchanged.

Reproduction: https://github.com/nateberkopec/nio4r-leak

Validation

  • Linux aarch64, Ruby 3.3.10, Valgrind 3.19, using the existing repro:

    • stock nio4r, 0 / 100 / 1000 selector closes: 0 / 2,300 / 23,000 bytes definitely lost from selector teardown
    • with this patch, 1000 closes: 0 bytes definitely lost from selector teardown
    • a fresh Valgrind run against the exact PR candidate also reported 0 definitely-lost selector teardown bytes after 1000 epoll closes
    • Valgrind still reports other findings in Ruby itself. Those are unrelated to this change, so the run is not globally clean.
  • macOS, Ruby 4.0.7: native extension builds. 1000 cycles each of register IO / select / deregister / double-close passed for :poll, :kqueue and :select.

  • Apart from this change, the ext and lib gem trees match upstream HEAD exactly.

  • Full upstream RSpec suite, against this PR's native extension with RSpec 3.13 (within the declared ~> 3.7 requirement):

    • macOS arm64, Ruby 4.0.7, kqueue: 112 examples, 0 failures, 2 pending (seed 23748)
    • Linux aarch64, Ruby 3.3.10, epoll: 112 examples, 0 failures, 2 pending (seed 44240)
    • Pending examples use the suite's existing UDP/TCP/TLS socket-fixture guards; no tests were excluded or modified.

No new tests were added.

@ioquatix
ioquatix merged commit 1504c5e into socketry:main Oct 9, 2026
27 checks passed
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.

leaking 23 bytes per close

2 participants