Hello,

I've been re-writing the polling mechanisms in the Erlang VM and stumbled
across
something that might be a bug in the OpenBSD implementation of kqueue.

When using EV_DISPATCH, the event is never triggered again after the EV_EOF
flag has been delivered, even though there is more data to be read from the
socket.

I've attached a smallish program that shows the problem.

The shortened ktrace output looks like this on OpenBSD 6.2:

 29672 a.out    0.012883 CALL  kevent(4,0x7f7ffffe8220,1,0,0,0)
 29672 a.out    0.012888 STRU  struct kevent { ident=5, filter=EVFILT_READ,
flags=0x81<EV_ADD|EV_DISPATCH>, fflags=0<>, data=0, udata=0x0 }
 29672 a.out    0.012895 RET   kevent 0
 29672 a.out    0.012904 CALL  kevent(4,0,0,0x7f7ffffe7cf0,32,0)
 29672 a.out    0.013408 STRU  struct kevent { ident=5, filter=EVFILT_READ,
flags=0x81<EV_ADD|EV_DISPATCH>, fflags=0<>, data=6, udata=0x0 }
 29672 a.out    0.013493 RET   kevent 1
 29672 a.out    0.013548 CALL  read(5,0x7f7ffffe8286,0x2)
 29672 a.out    0.013562 RET   read 2
 29672 a.out    0.013590 CALL  kevent(4,0x7f7ffffe8220,1,0,0,0)
 29672 a.out    0.013594 STRU  struct kevent { ident=5, filter=EVFILT_READ,
flags=0x84<EV_ENABLE|EV_DISPATCH>, fflags=0<>, data=0, udata=0x0 }
 29672 a.out    0.013608 RET   kevent 0
 29672 a.out    1.022228 CALL  kevent(4,0,0,0x7f7ffffe7cf0,32,0)
 29672 a.out    1.022537 STRU  struct kevent { ident=5, filter=EVFILT_READ,
flags=0x8081<EV_ADD|EV_DISPATCH|EV_EOF>, fflags=0<>, data=4, udata=0x0 }
 29672 a.out    1.022572 RET   kevent 1
 29672 a.out    1.022663 CALL  read(5,0x7f7ffffe8286,0x2)
 29672 a.out    1.022707 RET   read 2
 29672 a.out    1.022816 CALL  kevent(4,0x7f7ffffe8220,1,0,0,0)
 29672 a.out    1.022822 STRU  struct kevent { ident=5, filter=EVFILT_READ,
flags=0x84<EV_ENABLE|EV_DISPATCH>, fflags=0<>, data=0, udata=0x0 }
 29672 a.out    1.022835 RET   kevent 0
 29672 a.out    2.032238 CALL  kevent(4,0,0,0x7f7ffffe7cf0,32,0)
 29672 a.out    5.277194 PSIG  SIGINT SIG_DFL

In this example I would have expected the last kevent call to return with
EV_EOF and
data set to 2, but it does not trigger again. If I don't use EV_DISPATCH,
the event is
triggered again and the program terminates.

Does anyone know if this is the expected behavior or a bug?

I've worked around this issue by using EV_ONESHOT instead of EV_DISPATCH on
OpenBSD for now, but would like to use EV_DISPATCH in the future as I've
found
that it aligns better with the abstractions that I use, and could possibly
be a little bit
more performant.

Lukas

PS. If relevant, it seems like FreeBSD does behave the way that I expected,
i.e.
it triggers again for EV_DISPATCH after EV_EOF has been shown. DS.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <err.h>
#include <sys/event.h>

#include <sys/socket.h>
#include <sys/un.h>
#include <netdb.h>
#include <stdio.h>
#include <stdarg.h>
#include <string.h>

#define USE_DISPATCH 1

int main() {
    struct addrinfo *addr;
    struct addrinfo hints;
    int kq, listen_s, fd = -1;
    struct kevent evSet;
    struct kevent evList[32];

    /* open a TCP socket */
    memset(&hints, 0, sizeof hints);
    hints.ai_family = PF_UNSPEC; /* any supported protocol */
    hints.ai_flags = AI_PASSIVE; /* result for bind() */
    hints.ai_socktype = SOCK_STREAM; /* TCP */
    int error = getaddrinfo ("127.0.0.1", "8080", &hints, &addr);
    if (error)
        errx(1, "getaddrinfo failed: %s", gai_strerror(error));
    listen_s = socket(addr->ai_family, addr->ai_socktype, addr->ai_protocol);
    if (setsockopt(listen_s, SOL_SOCKET, SO_REUSEADDR, &(int){ 1 }, sizeof(int)) < 0)
        errx(1, "setsockopt(SO_REUSEADDR) failed");
    bind(listen_s, addr->ai_addr, addr->ai_addrlen);
    listen(listen_s, 5);

    kq = kqueue();

    system("echo -n abcdef | nc -v -w 1 127.0.0.1 8080 &");

    EV_SET(&evSet, listen_s, EVFILT_READ, EV_ADD, 0, 0, NULL);
    if (kevent(kq, &evSet, 1, NULL, 0, NULL) == -1)
        err(1, "kevent");

    while(1) {
        int i;
        int nev = kevent(kq, NULL, 0, evList, 32, NULL);
        for (i = 0; i < nev; i++) {
            if (evList[i].ident == listen_s) {
                struct sockaddr_storage addr;
                socklen_t socklen = sizeof(addr);
                if (fd != -1)
                    close(fd);
                fd = accept(evList[i].ident, (struct sockaddr *)&addr, &socklen);
                printf("accepted %d\n", fd);
#if USE_DISPATCH
                EV_SET(&evSet, fd, EVFILT_READ, EV_ADD|EV_DISPATCH, 0, 0, NULL);
#else
                EV_SET(&evSet, fd, EVFILT_READ, EV_ADD, 0, 0, NULL);
#endif
                if (kevent(kq, &evSet, 1, NULL, 0, NULL) == -1)
                    err(1, "kevent");
            } else {
                if (evList[i].flags & EV_EOF && evList[i].data == 0) {
                    printf("closing %d\n", fd);
                    close(fd);
                    fd = -1;
                    exit(0);
                } else if (evList[i].filter == EVFILT_READ) {
                    char buff[2];
                    read(fd, buff, 2);
                    if (evList[i].flags & EV_EOF) printf("EOF: ");
                    printf("read %c%c from %d\n", buff[0], buff[1], fd);
#if USE_DISPATCH
                    EV_SET(&evSet, fd, EVFILT_READ, EV_ENABLE|EV_DISPATCH, 0, 0, NULL);
                    if (kevent(kq, &evSet, 1, NULL, 0, NULL) == -1)
                        err(1, "kevent");
#endif
                    sleep(1);
                }
            }
        }
    }
}

Reply via email to