I thought long and hard about this problem when writing luv. It's a hard requirement that the userdata must be referenced in lua somewhere as long as there is any change the callback will be called and require the uv handle. There are two possible solutions to this.
The first is what I do in Luv currently. I ref the userdata upon handle creation and unref it during the close callback. This means that the user has to remember to call close on the handle or it will leak uv and lua resources. But as long as they remember to call close, the lua side will be able to be garbage collected. This method is simple and not prone to errors, or segfaults. It's trivial for unit tests to check the list of handles in a loop before and after a piece of code and fail the unit test if there are any unclosed handles. This find any library calls that forget to cleanup after themselves.
The other idea is much more complicated. There are times when a handle has no references in any lua code and will never emit any callbacks either. In this case, it would be nice to trigger the GC somehow and then call uv_close for the user. The problem is this is complex and different for each type. I went down this path but gave up because it was too much complex code. The basic idea is to only ref the userdata when there is at least one possible callback pending and to unref it as soon as you know no callbacks can happen. For timers, for example, `uv_timer_start` would require a ref, you can't then unref it till the user calls `uv_timer_stop`. But then what about timers with an interval of 0 after the first timeout? What about uv_timer_again? It gets very complex very quick and in many cases you can't unref before the close callback anyway.
If you go the second route, you still have the problem of how to hold the reference between the time of `uv_close` and it's callback. Lua will free the memory right after your close returns if using full userdata. Then in your close callback you will have to be very careful because the handle will probably be an invalid pointer. Some code internal to libuv might assume the handle is good any time before calling uv_close and segfault on it's own.