[PATCH] Events: protection from stale changes to level-triggered kevents

Sergey Kandaurov pluknet at nginx.com
Wed Jan 24 00:19:59 UTC 2024


# HG changeset patch
# User Sergey Kandaurov <pluknet at nginx.com>
# Date 1706055243 -14400
#      Wed Jan 24 04:14:03 2024 +0400
# Node ID d47ed07b06e93f4c6137ccd4ddfce0de23afb6a2
# Parent  ee40e2b1d0833b46128a357fbc84c6e23be9be07
Events: protection from stale changes to level-triggered kevents.

When kqueue events are reported in level-triggered mode, without EV_CLEAR set,
it was previously possible to try to remove a kevent attached to a closed file
descriptor.  Calling close() on a file descriptor removes any kevents that
reference the descriptor, it is an error to remove such kevents afterwards.

In FreeBSD, this results in a kevent reported with EV_ERROR set in flags and
EBADF in data, which corresponds to operating on an invalid file descriptor.
MacOS behaves similarly; the difference is that it uses a distinct error path
for no knote found and EV_ADD unspecified, and returns EV_ERROR with ENOENT.
Either way, this triggers "kevent() error" alerts.

In practice, this may happen as part of handing read event after the main loop
in ngx_event_pipe(), which then initiates closing the connection, as caught by
proxy_chunked_extra.t.

Another use-case common on SSL connections is handling read event after SSL
handshaking is finished, which results in a kevent removal change.  It may
happen then to fully read and process the request in the same cycle iteration,
closing the connection with the pending kevent removal.  A variation of this
use-case is to re-add the event after SSL handshaking to read SSL payload,
e.g. as part of the application protocol greeting, then receive EPIPE from a
subsequent SSL_write() and remove the event again on connection close.
Normally this would result in three change list elements appended: EV_DELETE,
EV_ADD, EV_DELETE.  The check in ngx_kqueue_del_event() annihilates instead
a previously appended EV_ADD change, leaving the first remove change, which
reduces to the previous use-case.  Caught by mail_ssl_session_reuse.t.

The fix is to check in ngx_kqueue_process_events() if we operate over a just
closed file descriptor in this iteration, as it may happen after the change
list was updated, and prune such kevent changes before entering kevent().

Another use-case that makes the fix incomplete is processing further events
in the same iteration that may reuse a just closed file descriptor and clear
ev->closed while initializing a reused event, rendering the check useless.
This may happen e.g. as part of accepting a new connection.  The fix is to
check in ngx_kqueue_add_event() if there are invalidated events in the change
list matching the one being added and prune the corresponding kevent changes.

diff --git a/src/event/modules/ngx_kqueue_module.c b/src/event/modules/ngx_kqueue_module.c
--- a/src/event/modules/ngx_kqueue_module.c
+++ b/src/event/modules/ngx_kqueue_module.c
@@ -283,8 +283,11 @@ static ngx_int_t
 ngx_kqueue_add_event(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags)
 {
     ngx_int_t          rc;
+#if !(NGX_HAVE_CLEAR_EVENT)
+    ngx_uint_t         i;
+    ngx_event_t       *e;
+#endif
 #if 0
-    ngx_event_t       *e;
     ngx_connection_t  *c;
 #endif
 
@@ -329,6 +332,36 @@ ngx_kqueue_add_event(ngx_event_t *ev, ng
 
 #endif
 
+#if !(NGX_HAVE_CLEAR_EVENT)
+
+    for (i = 0; i < nchanges; i++) {
+        if (ev->index == NGX_INVALID_INDEX
+            && ((uintptr_t) change_list[i].udata & (uintptr_t) ~1)
+                == (uintptr_t) ev)
+        {
+            ngx_log_debug3(NGX_LOG_DEBUG_EVENT, ev->log, 0,
+                           "kevent stale: %d: ft:%d fl:%04Xd",
+                           (int) change_list[i].ident, change_list[i].filter,
+                           change_list[i].flags);
+
+            /*
+             * the stale event from a file descriptor
+             * that was just closed and reused in this iteration
+             */
+
+            if (i < --nchanges) {
+                e = (ngx_event_t *)
+                    ((uintptr_t) change_list[nchanges].udata & (uintptr_t) ~1);
+                change_list[i] = change_list[nchanges];
+                e->index = i;
+
+                i--;
+            }
+        }
+    }
+
+#endif
+
     rc = ngx_kqueue_set_event(ev, event, EV_ADD|EV_ENABLE|flags);
 
     return rc;
@@ -503,6 +536,9 @@ ngx_kqueue_process_events(ngx_cycle_t *c
     ngx_uint_t        level;
     ngx_err_t         err;
     ngx_event_t      *ev;
+#if !(NGX_HAVE_CLEAR_EVENT)
+    ngx_event_t      *e;
+#endif
     ngx_queue_t      *queue;
     struct timespec   ts, *tp;
 
@@ -530,6 +566,36 @@ ngx_kqueue_process_events(ngx_cycle_t *c
         tp = &ts;
     }
 
+#if !(NGX_HAVE_CLEAR_EVENT)
+
+    for (i = 0; i < n; i++) {
+        ev = (ngx_event_t *)
+                           ((uintptr_t) change_list[i].udata & (uintptr_t) ~1);
+
+        if (ev->closed) {
+            ngx_log_debug3(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
+                           "kevent closed: %d: ft:%d fl:%04Xd",
+                           (int) change_list[i].ident, change_list[i].filter,
+                           change_list[i].flags);
+
+            /*
+             * the stale event change for a file descriptor
+             * that was just closed in this iteration
+             */
+
+            if (i < --n) {
+                e = (ngx_event_t *)
+                           ((uintptr_t) change_list[n].udata & (uintptr_t) ~1);
+                change_list[i] = change_list[n];
+                e->index = i;
+
+                i--;
+            }
+        }
+    }
+
+#endif
+
     ngx_log_debug2(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                    "kevent timer: %M, changes: %d", timer, n);
 


More information about the nginx-devel mailing list