Skip to content

Commit 41d7cd3

Browse files
committed
Fix unchecked kevent return values in kqueue ASIO backend
The kqueue backend silently ignored failures when registering event filters. With a NULL eventlist, kevent stops on the first changelist failure, leaving partial registrations and no actor notification. Actors that thought they were subscribed would wait forever. Use EV_RECEIPT so all changelist entries are processed regardless of individual failures, and send ASIO_ERROR to the owning actor on subscription failure. All seven stdlib AsioEventNotify consumers handle the new flag by tearing down through their existing fatal-error paths. Also fixes unchecked pipe() and kevent() returns in ponyint_asio_backend_init, removes dead EV_ERROR code from the dispatch loop, and adds missing ponyint_messageq_destroy on kqueue() failure. Closes #5051
1 parent a125b78 commit 41d7cd3

File tree

11 files changed

+165
-27
lines changed

11 files changed

+165
-27
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
## Fix unchecked kevent return values in kqueue ASIO backend
2+
3+
On macOS and BSD, the kqueue ASIO backend silently ignored failures when registering event filters. When `pony_asio_event_subscribe` submitted multiple filters (e.g., READ and WRITE) and one failed, the remaining filters were never registered. The actor had no way to detect this, resulting in events that would never fire and connections that would hang indefinitely.
4+
5+
The backend now uses `EV_RECEIPT` to ensure all filter registrations are processed regardless of individual failures, and sends a new `ASIO_ERROR` notification to the owning actor when a subscription fails. All stdlib consumers (`TCPConnection`, `TCPListener`, `UDPSocket`, `Stdin`, `ProcessMonitor`, `SignalHandler`, `Timers`) treat `ASIO_ERROR` as a fatal event and tear down, the same as any other I/O failure.
6+
7+
If you implement `AsioEventNotify` outside the stdlib, you should add handling for `ASIO_ERROR`. Without it, a subscription failure is silently ignored (the same behavior as before this fix, but now you have the option to detect it).
8+
9+
The `AsioEvent` primitive gains a new `errored` predicate method for checking the flag:
10+
11+
```pony
12+
be _event_notify(event: AsioEventID, flags: U32, arg: U32) =>
13+
if AsioEvent.errored(flags) then
14+
// Subscription failed — tear down
15+
_close()
16+
return
17+
end
18+
// ... normal event handling
19+
```

packages/builtin/asio_event.pony

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ primitive AsioEvent
3131
"""
3232
flags == 0
3333

34+
fun errored(flags: U32): Bool =>
35+
"""
36+
Returns true if the flags contain the error flag, indicating a
37+
subscription failure. The event is compromised and the actor should
38+
tear down.
39+
"""
40+
(flags and (1 << 4)) != 0
41+
3442
fun oneshotable(flags: U32): Bool =>
3543
"""
3644
Returns true if the flags contain the oneshot flag.

packages/builtin/stdin.pony

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,10 @@ actor Stdin is AsioEventNotify
122122
"""
123123
if AsioEvent.disposable(flags) then
124124
@pony_asio_event_destroy(event)
125+
elseif (_event is event) and AsioEvent.errored(flags) then
126+
_close_event()
127+
try (_notify as InputNotify).dispose() end
128+
_notify = None
125129
elseif (_event is event) and AsioEvent.readable(flags) then
126130
_read()
127131
end

packages/net/tcp_connection.pony

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,22 @@ actor TCPConnection is AsioEventNotify
638638
@pony_os_socket_close(fd)
639639
_try_shutdown()
640640
end
641+
elseif AsioEvent.errored(flags) then
642+
// A subscription failure on a connection attempt.
643+
var fd = @pony_asio_event_fd(event)
644+
_connect_count = _connect_count - 1
645+
646+
if not _connected and not _closed then
647+
@pony_asio_event_unsubscribe(event)
648+
@pony_os_socket_close(fd)
649+
_notify_connecting()
650+
else
651+
if not @pony_asio_event_get_disposable(event) then
652+
@pony_asio_event_unsubscribe(event)
653+
end
654+
@pony_os_socket_close(fd)
655+
_try_shutdown()
656+
end
641657
else
642658
// It's not our event.
643659
if AsioEvent.disposable(flags) then
@@ -647,6 +663,11 @@ actor TCPConnection is AsioEventNotify
647663
end
648664
else
649665
// At this point, it's our event.
666+
if AsioEvent.errored(flags) then
667+
hard_close()
668+
return
669+
end
670+
650671
if AsioEvent.writeable(flags) then
651672
_writeable = true
652673
_complete_writes(arg)

packages/net/tcp_listener.pony

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,11 @@ actor TCPListener is AsioEventNotify
150150
return
151151
end
152152

153+
if AsioEvent.errored(flags) then
154+
close()
155+
return
156+
end
157+
153158
if AsioEvent.readable(flags) then
154159
_accept(arg)
155160
end

packages/net/udp_socket.pony

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,11 @@ actor UDPSocket is AsioEventNotify
261261
end
262262

263263
if not _closed then
264+
if AsioEvent.errored(flags) then
265+
_close()
266+
return
267+
end
268+
264269
if AsioEvent.readable(flags) then
265270
_readable = true
266271
_complete_reads(arg)

packages/process/process_monitor.pony

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,11 @@ actor ProcessMonitor is AsioEventNotify
221221
"""
222222
Handle the incoming Asio event from one of the pipes.
223223
"""
224+
if AsioEvent.errored(flags) then
225+
_close()
226+
return
227+
end
228+
224229
match event
225230
| _stdin.event =>
226231
if AsioEvent.writeable(flags) then

packages/signals/signal_handler.pony

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ actor SignalHandler is AsioEventNotify
4545
"""
4646
if AsioEvent.disposable(flags) then
4747
@pony_asio_event_destroy(event)
48+
elseif (event is _event) and AsioEvent.errored(flags) then
49+
_dispose()
4850
elseif event is _event then
4951
if not _notify(arg) then
5052
_dispose()

packages/time/timers.pony

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,13 @@ actor Timers is AsioEventNotify
8181
"""
8282
if AsioEvent.disposable(flags) then
8383
@pony_asio_event_destroy(event)
84+
elseif (event is _event) and AsioEvent.errored(flags) then
85+
for wheel in _wheel.values() do
86+
wheel.clear()
87+
end
88+
_map.clear()
89+
@pony_asio_event_unsubscribe(_event)
90+
_event = AsioEvent.none()
8491
elseif event is _event then
8592
_advance()
8693
end

src/libponyrt/asio/asio.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ enum
3232
ASIO_WRITE = 1 << 1,
3333
ASIO_TIMER = 1 << 2,
3434
ASIO_SIGNAL = 1 << 3,
35+
ASIO_ERROR = 1 << 4,
3536
ASIO_ONESHOT = 1 << 8,
3637
ASIO_DESTROYED = (uint32_t)-1
3738
};

0 commit comments

Comments
 (0)