Fix dangling path pointer in StatWatcher under GC compaction#98
Merged
Watson1978 merged 2 commits intoJul 2, 2026
Merged
Conversation
ev_stat_init() retains the path pointer it is given for the entire lifetime of the watcher; libev dereferences it on every stat (inotify rescans, lstat, statfs, ...). StatWatcher passed RSTRING_PTR(@path), which points into a Ruby String whose buffer GC.compact may relocate, leaving libev with a dangling pointer. Keep an owned copy of the path in the watcher struct instead, and free it in a new Coolio_Watcher_free() (replacing RUBY_DEFAULT_FREE). The copy is stable memory that the GC never moves, so libev's retained pointer stays valid. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
RSTRING_PTR does not, by contract, guarantee a NUL terminator, and it does nothing about embedded NUL bytes: a path such as "foo\0bar" would be silently truncated to "foo" when libev treats it as a C string. Copy from StringValueCStr(path) instead, which guarantees NUL termination and raises ArgumentError on an embedded NUL, matching how the standard library handles paths (e.g. File.stat). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1f2531c to
75ae12e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
ev_stat_init()retains thepathpointer it is given for the entire lifetime of the watcher, and libev dereferences it on every stat (inotify rescans,lstat,statfs, …).Coolio::StatWatcherpassedRSTRING_PTR(@path), which points into a RubyStringwhose backing bufferGC.compactmay relocate. After a compaction, libev is left holding a dangling pointer and maystat()freed/relocated memory — watching the wrong path or, in the worst case, crashing.This is latent (its manifestation depends on GC timing and libev's access pattern), but it is a genuine object-lifetime hazard: a raw
char*obtained fromRSTRING_PTRmust not be retained across code that can trigger compaction.Fix
Keep an owned copy of the path in the watcher struct (
stat_path) and hand that toev_stat_init(), so libev points at stable memory the GC never moves. The copy is released in a newCoolio_Watcher_free()that replacesRUBY_DEFAULT_FREE(non-stat watchers leavestat_pathasNULL, so the free is a no-op for them).strlcpyis used for the copy; it is available on all Ruby-supported platforms viaruby/missing.h's fallback declaration, so noextconf.rbchange is needed.Test
Adds a spec that runs a
StatWatcheracross a forced compaction (GC.verify_compaction_references/GC.compact) and assertson_changestill fires.Note: this is primarily a smoke/regression guard — because libev's retained pointer is not a Ruby-visible reference and compaction reuses (rather than unmaps) the vacated slot, the pre-fix bug cannot be surfaced deterministically from pure Ruby. The correctness of the fix rests on owning the buffer.
Full suite passes locally (Ruby 4.0.5), including the shared-
freememory-leak spec.🤖 Generated with Claude Code