about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2024-09-23 19:51:20 +0200
committerbptato <nincsnevem662@gmail.com>2024-09-23 19:58:54 +0200
commitfcd9aa9f9c604ed5d104343542962a26b2acda62 (patch)
tree7b0eea9b63bacc27cdc6471e2b2409b7b5d15c9e
parent8e8c7f0911f4a20446a83090d722fecaf203f6f3 (diff)
downloadchawan-fcd9aa9f9c604ed5d104343542962a26b2acda62.tar.gz
Replace std/selectors with poll
std/selectors uses OS-specific selector APIs, which sounds good in
theory (faster than poll!), but sucks for portability in practice.
Sure, you can fix portability bugs, but who knows how many there are
on untested platforms... poll is standard, so if it works on one
computer it should work on all other ones. (I hope.)

As a bonus, I rewrote the timeout API for poll, which incidentally
fixes setTimeout across forks. Also, SIGWINCH should now work on all
platforms (as we self-pipe instead of signalfd/kqueue magic).
-rw-r--r--Makefile2
-rw-r--r--adapter/format/ansi2html.nim35
-rw-r--r--src/html/env.nim33
-rw-r--r--src/io/bufreader.nim1
-rw-r--r--src/io/bufwriter.nim1
-rw-r--r--src/io/dynstream.nim9
-rw-r--r--src/io/poll.nim40
-rw-r--r--src/js/timeout.nim113
-rw-r--r--src/loader/loader.nim63
-rw-r--r--src/local/client.nim130
-rw-r--r--src/local/pager.nim11
-rw-r--r--src/server/buffer.nim161
-rw-r--r--src/server/forkserver.nim7
-rw-r--r--src/utils/sandbox.nim13
14 files changed, 316 insertions, 303 deletions
diff --git a/Makefile b/Makefile
index 80a59e4b..1b5b7857 100644
--- a/Makefile
+++ b/Makefile
@@ -119,7 +119,7 @@ $(OUTDIR_CGI_BIN)/resize: adapter/img/stb_image_resize.h adapter/img/stb_image_r
 	src/utils/sandbox.nim $(dynstream) $(twtstr)
 $(OUTDIR_LIBEXEC)/urlenc: $(twtstr)
 $(OUTDIR_LIBEXEC)/gopher2html: adapter/gophertypes.nim $(twtstr)
-$(OUTDIR_LIBEXEC)/ansi2html: src/types/color.nim $(twtstr)
+$(OUTDIR_LIBEXEC)/ansi2html: src/types/color.nim src/io/poll.nim $(twtstr) $(dynstream)
 $(OUTDIR_LIBEXEC)/md2html: $(twtstr)
 
 $(OUTDIR_CGI_BIN)/%: adapter/protocol/%.nim
diff --git a/adapter/format/ansi2html.nim b/adapter/format/ansi2html.nim
index dcbc4210..9f24dd0b 100644
--- a/adapter/format/ansi2html.nim
+++ b/adapter/format/ansi2html.nim
@@ -1,8 +1,9 @@
 import std/options
 import std/os
 import std/posix
-import std/selectors
 
+import io/dynstream
+import io/poll
 import types/color
 import utils/twtstr
 
@@ -381,27 +382,23 @@ proc main() =
   if standalone:
     state.puts("<body>\n")
   state.puts("<pre style='margin: 0'>\n")
-  let ofl = fcntl(STDIN_FILENO, F_GETFL, 0)
-  doAssert ofl != -1
-  discard fcntl(STDIN_FILENO, F_SETFL, ofl and not O_NONBLOCK)
+  let ps = newPosixStream(STDIN_FILENO)
+  ps.setBlocking(false)
   var buffer {.noinit.}: array[4096, char]
-  var selector = newSelector[int]()
-  block mainloop:
-    while true:
-      let n = read(STDIN_FILENO, addr buffer[0], buffer.high)
-      if n != -1:
-        if n == 0:
-          break
-        state.processData(buffer.toOpenArray(0, n - 1))
-      else:
-        doAssert errno == EAGAIN or errno == EWOULDBLOCK
-        state.flushOutbuf()
-        selector.registerHandle(STDIN_FILENO, {Read}, 0)
-        discard selector.select(-1)
-        selector.unregister(STDIN_FILENO)
+  var pollData = PollData()
+  while true:
+    try:
+      let n = ps.recvData(buffer)
+      if n == 0:
+        break
+      state.processData(buffer.toOpenArray(0, n - 1))
+    except ErrorAgain:
+      state.flushOutbuf()
+      pollData.register(ps.fd, POLLIN)
+      pollData.poll(-1)
+      pollData.unregister(ps.fd)
   if standalone:
     state.puts("</body>")
   state.flushOutbuf()
-  discard fcntl(STDIN_FILENO, F_SETFL, ofl)
 
 main()
diff --git a/src/html/env.nim b/src/html/env.nim
index ab9a9be5..27a12816 100644
--- a/src/html/env.nim
+++ b/src/html/env.nim
@@ -1,4 +1,3 @@
-import std/selectors
 import std/tables
 
 import html/catom
@@ -265,25 +264,22 @@ proc addWindowModule2*(ctx: JSContext) =
   ctx.registerType(Window, parent = eventTargetCID, asglobal = true,
     globalparent = true)
 
-proc addScripting*(window: Window; selector: Selector[int]) =
+proc evalJSFree(opaque: RootRef; src, file: string) =
+  let window = Window(opaque)
+  let ret = window.jsctx.eval(src, file, JS_EVAL_TYPE_GLOBAL)
+  if JS_IsException(ret):
+    window.console.log("Exception in document", $window.document.url,
+      window.jsctx.getExceptionMsg())
+  else:
+    JS_FreeValue(window.jsctx, ret)
+
+proc addScripting*(window: Window) =
   let rt = newJSRuntime()
   let ctx = rt.newJSContext()
   window.jsrt = rt
   window.jsctx = ctx
   window.importMapsAllowed = true
-  window.timeouts = newTimeoutState(
-    selector = selector,
-    jsctx = ctx,
-    err = window.console.err,
-    evalJSFree = (proc(src, file: string) =
-      let ret = window.jsctx.eval(src, file, JS_EVAL_TYPE_GLOBAL)
-      if JS_IsException(ret):
-        window.console.log("Exception in document", $window.document.url,
-          window.jsctx.getExceptionMsg())
-      else:
-        JS_FreeValue(ctx, ret)
-    )
-  )
+  window.timeouts = newTimeoutState(ctx, window.console.err, evalJSFree, window)
   ctx.addWindowModule()
   ctx.setGlobal(window)
   ctx.addDOMExceptionModule()
@@ -309,9 +305,8 @@ proc runJSJobs*(window: Window) =
     let ctx = r.error
     ctx.writeException(window.console.err)
 
-proc newWindow*(scripting, images, styling: bool; selector: Selector[int];
-    attrs: WindowAttributes; factory: CAtomFactory; loader: FileLoader;
-    url: URL): Window =
+proc newWindow*(scripting, images, styling: bool; attrs: WindowAttributes;
+    factory: CAtomFactory; loader: FileLoader; url: URL): Window =
   let err = newDynFileStream(stderr)
   let window = Window(
     attrs: attrs,
@@ -328,5 +323,5 @@ proc newWindow*(scripting, images, styling: bool; selector: Selector[int];
   )
   window.location = window.newLocation()
   if scripting:
-    window.addScripting(selector)
+    window.addScripting()
   return window
diff --git a/src/io/bufreader.nim b/src/io/bufreader.nim
index 46f8e0f3..718e6d55 100644
--- a/src/io/bufreader.nim
+++ b/src/io/bufreader.nim
@@ -1,5 +1,4 @@
 import std/options
-import std/sets
 import std/tables
 
 import io/dynstream
diff --git a/src/io/bufwriter.nim b/src/io/bufwriter.nim
index 77b8ebd8..3eea01fa 100644
--- a/src/io/bufwriter.nim
+++ b/src/io/bufwriter.nim
@@ -2,7 +2,6 @@
 # Each packet is prefixed with its length as a pointer-sized integer.
 
 import std/options
-import std/sets
 import std/tables
 
 import io/dynstream
diff --git a/src/io/dynstream.nim b/src/io/dynstream.nim
index 66a2a932..b0b07140 100644
--- a/src/io/dynstream.nim
+++ b/src/io/dynstream.nim
@@ -251,6 +251,15 @@ proc dealloc*(mem: MaybeMappedMemory) =
   else:
     dealloc(mem.p0)
 
+proc drain*(ps: PosixStream) =
+  assert not ps.blocking
+  var buffer {.noinit.}: array[4096, uint8]
+  try:
+    while true:
+      discard ps.recvData(addr buffer[0], buffer.len)
+  except ErrorAgain:
+    discard
+
 type SocketStream* = ref object of PosixStream
   source*: Socket
 
diff --git a/src/io/poll.nim b/src/io/poll.nim
new file mode 100644
index 00000000..3c2c29a8
--- /dev/null
+++ b/src/io/poll.nim
@@ -0,0 +1,40 @@
+import std/posix
+
+type PollData* = object
+  fds: seq[TPollFd]
+
+iterator events*(ctx: PollData): TPollFd =
+  let L = ctx.fds.len
+  for i in 0 ..< L:
+    let event = ctx.fds[i]
+    if event.fd == -1 or ctx.fds[i].revents == 0:
+      continue
+    assert (event.revents and POLLNVAL) == 0
+    yield event
+
+proc register*(ctx: var PollData; fd: int; events: cshort) =
+  if fd >= ctx.fds.len:
+    ctx.fds.setLen(fd + 1)
+  ctx.fds[fd].fd = cint(fd)
+  ctx.fds[fd].events = events
+
+proc register*(ctx: var PollData; fd: cint; events: cshort) =
+  ctx.register(int(fd), events)
+
+proc unregister*(ctx: var PollData; fd: int) =
+  ctx.fds[fd].fd = -1
+
+proc trim(ctx: var PollData) =
+  var i = ctx.fds.high
+  while i >= 0:
+    if ctx.fds[i].fd != -1:
+      break
+    dec i
+  ctx.fds.setLen(i + 1)
+
+proc clear*(ctx: var PollData) =
+  ctx.fds.setLen(0)
+
+proc poll*(ctx: var PollData; timeout: cint) =
+  ctx.trim()
+  discard poll(addr ctx.fds[0], Tnfds(ctx.fds.len), cint(timeout))
diff --git a/src/js/timeout.nim b/src/js/timeout.nim
index 0213156a..72a68dbc 100644
--- a/src/js/timeout.nim
+++ b/src/js/timeout.nim
@@ -1,5 +1,5 @@
-import std/selectors
-import std/tables
+import std/algorithm
+import std/times
 
 import io/dynstream
 import js/console
@@ -15,59 +15,76 @@ type
 
   TimeoutEntry = ref object
     t: TimeoutType
-    fd: int
+    id: int32
     val: JSValue
     args: seq[JSValue]
+    expires: int64
+    timeout: int32
+
+  EvalJSFree* = proc(opaque: RootRef; src, file: string) {.nimcall.}
 
   TimeoutState* = ref object
     timeoutid: int32
-    timeouts: Table[int32, TimeoutEntry]
-    timeoutFds: Table[int, int32]
-    selector: Selector[int] #TODO would be better with void...
+    timeouts: seq[TimeoutEntry]
     jsctx: JSContext
     err: DynStream #TODO shouldn't be needed
-    evalJSFree: proc(src, file: string) #TODO ew
+    evalJSFree: EvalJSFree
+    opaque: RootRef
+    sorted: bool
 
-func newTimeoutState*(selector: Selector[int]; jsctx: JSContext; err: DynStream;
-    evalJSFree: proc(src, file: string)): TimeoutState =
+func newTimeoutState*(jsctx: JSContext; err: DynStream;
+    evalJSFree: EvalJSFree; opaque: RootRef): TimeoutState =
   return TimeoutState(
-    selector: selector,
     jsctx: jsctx,
     err: err,
-    evalJSFree: evalJSFree
+    evalJSFree: evalJSFree,
+    opaque: opaque,
+    sorted: true
   )
 
 func empty*(state: TimeoutState): bool =
   return state.timeouts.len == 0
 
+proc clearTimeout0(state: var TimeoutState; i: int) =
+  let entry = state.timeouts[i]
+  JS_FreeValue(state.jsctx, entry.val)
+  for arg in entry.args:
+    JS_FreeValue(state.jsctx, arg)
+  state.timeouts.del(i)
+  if state.timeouts.len != i: # only set if we del'd in the middle
+    state.sorted = false
+
 proc clearTimeout*(state: var TimeoutState; id: int32) =
-  if id in state.timeouts:
-    let entry = state.timeouts[id]
-    state.selector.unregister(entry.fd)
-    JS_FreeValue(state.jsctx, entry.val)
-    for arg in entry.args:
-      JS_FreeValue(state.jsctx, arg)
-    state.timeoutFds.del(entry.fd)
-    state.timeouts.del(id)
+  var j = -1
+  for i in 0 ..< state.timeouts.len:
+    if state.timeouts[i].id == id:
+      j = i
+      break
+  if j != -1:
+    state.clearTimeout0(j)
+
+proc getUnixMillis(): int64 =
+  let now = getTime()
+  return now.toUnix() * 1000 + now.nanosecond div 1_000_000
 
-#TODO varargs
 proc setTimeout*(state: var TimeoutState; t: TimeoutType; handler: JSValue;
     timeout: int32; args: openArray[JSValue]): int32 =
   let id = state.timeoutid
   inc state.timeoutid
-  let fd = state.selector.registerTimer(max(timeout, 1), t == ttTimeout, 0)
-  state.timeoutFds[fd] = id
   let entry = TimeoutEntry(
     t: t,
-    fd: fd,
-    val: JS_DupValue(state.jsctx, handler)
+    id: id,
+    val: JS_DupValue(state.jsctx, handler),
+    expires: getUnixMillis() + int64(timeout),
+    timeout: timeout
   )
   for arg in args:
     entry.args.add(JS_DupValue(state.jsctx, arg))
-  state.timeouts[id] = entry
+  state.timeouts.add(entry)
+  state.sorted = false
   return id
 
-proc runEntry(state: var TimeoutState; entry: TimeoutEntry; name: string) =
+proc runEntry(state: var TimeoutState; entry: TimeoutEntry) =
   if JS_IsFunction(state.jsctx, entry.val):
     let ret = JS_Call(state.jsctx, entry.val, JS_UNDEFINED,
       cint(entry.args.len), entry.args.toJSValueArray())
@@ -77,23 +94,39 @@ proc runEntry(state: var TimeoutState; entry: TimeoutEntry; name: string) =
   else:
     var s: string
     if state.jsctx.fromJS(entry.val, s).isSome:
-      state.evalJSFree(s, name)
+      state.evalJSFree(state.opaque, s, $entry.t)
+
+# for poll
+proc sortAndGetTimeout*(state: var TimeoutState): cint =
+  if state.timeouts.len == 0:
+    return -1
+  if not state.sorted:
+    state.timeouts.sort(proc(a, b: TimeoutEntry): int =
+      cmp(a.expires, b.expires), order = Descending)
+    state.sorted = true
+  let now = getUnixMillis()
+  return cint(max(state.timeouts[^1].expires - now, -1))
 
-proc runTimeoutFd*(state: var TimeoutState; fd: int): bool =
-  if fd notin state.timeoutFds:
-    return false
-  let id = state.timeoutFds[fd]
-  let entry = state.timeouts[id]
-  state.runEntry(entry, $entry.t)
-  if entry.t == ttTimeout:
-    state.clearTimeout(id)
-  return true
+proc run*(state: var TimeoutState): bool =
+  let H = state.timeouts.high
+  let now = getUnixMillis()
+  var found = false
+  for i in countdown(H, 0):
+    if state.timeouts[i].expires > now:
+      break
+    let entry = state.timeouts[i]
+    state.runEntry(entry)
+    found = true
+    case entry.t
+    of ttTimeout: state.clearTimeout0(i)
+    of ttInterval:
+      entry.expires = now + entry.timeout
+      state.sorted = false
+  return found
 
 proc clearAll*(state: var TimeoutState) =
-  for entry in state.timeouts.values:
-    state.selector.unregister(entry.fd)
+  for entry in state.timeouts:
     JS_FreeValue(state.jsctx, entry.val)
     for arg in entry.args:
       JS_FreeValue(state.jsctx, arg)
-  state.timeouts.clear()
-  state.timeoutFds.clear()
+  state.timeouts.setLen(0)
diff --git a/src/loader/loader.nim b/src/loader/loader.nim
index 85490b84..e8d29ee2 100644
--- a/src/loader/loader.nim
+++ b/src/loader/loader.nim
@@ -26,13 +26,13 @@ import std/net
 import std/options
 import std/os
 import std/posix
-import std/selectors
 import std/strutils
 import std/tables
 
 import io/bufreader
 import io/bufwriter
 import io/dynstream
+import io/poll
 import io/serversocket
 import io/stdio
 import io/tempfile
@@ -42,7 +42,6 @@ import loader/headers
 import loader/loaderhandle
 import loader/loaderiface
 import loader/request
-import loader/response
 import monoucha/javascript
 import types/cookie
 import types/formdata
@@ -52,9 +51,6 @@ import types/urimethodmap
 import types/url
 import utils/twtstr
 
-export request
-export response
-
 type
   CachedItem = ref object
     id: int
@@ -77,7 +73,7 @@ type
     alive: bool
     config: LoaderConfig
     handleMap: seq[LoaderHandle]
-    selector: Selector[int]
+    pollData: PollData
     # List of existing clients (buffer or pager) that may make requests.
     clientData: Table[int, ClientData] # pid -> data
     # ID of next output. TODO: find a better allocation scheme
@@ -145,40 +141,22 @@ type PushBufferResult = enum
 
 proc register(ctx: LoaderContext; handle: InputHandle) =
   assert not handle.registered
-  ctx.selector.registerHandle(int(handle.stream.fd), {Read}, 0)
+  ctx.pollData.register(handle.stream.fd, cshort(POLLIN))
   handle.registered = true
 
 proc unregister(ctx: LoaderContext; handle: InputHandle) =
   assert handle.registered
-  ctx.selector.unregister(int(handle.stream.fd))
+  ctx.pollData.unregister(int(handle.stream.fd))
   handle.registered = false
 
 proc register(ctx: LoaderContext; output: OutputHandle) =
   assert not output.registered
-  ctx.selector.registerHandle(int(output.stream.fd), {Write}, 0)
+  ctx.pollData.register(int(output.stream.fd), cshort(POLLOUT))
   output.registered = true
 
-const bsdPlatform = defined(macosx) or defined(freebsd) or defined(netbsd) or
-  defined(openbsd) or defined(dragonfly)
 proc unregister(ctx: LoaderContext; output: OutputHandle) =
   assert output.registered
-  # so kqueue-based selectors raise when we try to unregister a pipe whose
-  # reader is at EOF. "solution": clean up this mess ourselves.
-  let fd = int(output.stream.fd)
-  when bsdPlatform:
-    let oc = ctx.selector.count
-    try:
-      ctx.selector.unregister(fd)
-    except IOSelectorsException:
-      # ????
-      for name, f in ctx.selector[].fieldPairs:
-        when name == "fds":
-          cast[ptr int](addr f[fd])[] = -1
-        elif name == "changes":
-          f.setLen(0)
-      ctx.selector.count = oc - 1
-  else:
-    ctx.selector.unregister(fd)
+  ctx.pollData.unregister(int(output.stream.fd))
   output.registered = false
 
 # Either write data to the target output, or append it to the list of buffers to
@@ -1178,17 +1156,13 @@ proc exitLoader(ctx: LoaderContext) =
 
 var gctx: LoaderContext
 proc initLoaderContext(fd: cint; config: LoaderConfig): LoaderContext =
-  var ctx = LoaderContext(
-    alive: true,
-    config: config,
-    selector: newSelector[int]()
-  )
+  var ctx = LoaderContext(alive: true, config: config)
   gctx = ctx
   let myPid = getCurrentProcessId()
   # we don't capsicumize loader, so -1 is appropriate here
   ctx.ssock = initServerSocket(config.sockdir, -1, myPid, blocking = true)
   let sfd = int(ctx.ssock.sock.getFd())
-  ctx.selector.registerHandle(sfd, {Read}, 0)
+  ctx.pollData.register(sfd, POLLIN)
   if sfd >= ctx.handleMap.len:
     ctx.handleMap.setLen(sfd + 1)
   ctx.handleMap[sfd] = LoaderHandle() # pseudo handle
@@ -1302,25 +1276,26 @@ proc finishCycle(ctx: LoaderContext; unregRead: var seq[InputHandle];
 proc runFileLoader*(fd: cint; config: LoaderConfig) =
   var ctx = initLoaderContext(fd, config)
   let fd = int(ctx.ssock.sock.getFd())
-  var keys: array[64, ReadyKey]
   while ctx.alive:
-    let count = ctx.selector.selectInto(-1, keys)
+    ctx.pollData.poll(-1)
     var unregRead: seq[InputHandle] = @[]
     var unregWrite: seq[OutputHandle] = @[]
-    for event in keys.toOpenArray(0, count - 1):
-      let handle = ctx.handleMap[event.fd]
-      if Read in event.events:
-        if event.fd == fd: # incoming connection
+    for event in ctx.pollData.events:
+      let efd = int(event.fd)
+      if (event.revents and POLLIN) != 0:
+        if efd == fd: # incoming connection
           ctx.acceptConnection()
         else:
-          let handle = InputHandle(ctx.handleMap[event.fd])
+          let handle = InputHandle(ctx.handleMap[efd])
           case ctx.handleRead(handle, unregWrite)
           of hrrDone: discard
           of hrrUnregister, hrrBrokenPipe: unregRead.add(handle)
-      if Write in event.events:
+      if (event.revents and POLLOUT) != 0:
+        let handle = ctx.handleMap[efd]
         ctx.handleWrite(OutputHandle(handle), unregWrite)
-      if Error in event.events:
-        assert event.fd != fd
+      if (event.revents and POLLERR) != 0 or (event.revents and POLLHUP) != 0:
+        assert efd != fd
+        let handle = ctx.handleMap[efd]
         if handle of InputHandle: # istream died
           unregRead.add(InputHandle(handle))
         else: # ostream died
diff --git a/src/local/client.nim b/src/local/client.nim
index 6c1d505c..f32e758d 100644
--- a/src/local/client.nim
+++ b/src/local/client.nim
@@ -4,7 +4,6 @@ import std/net
 import std/options
 import std/os
 import std/posix
-import std/selectors
 import std/strutils
 import std/tables
 
@@ -19,6 +18,7 @@ import html/formdata
 import html/xmlhttprequest
 import io/bufwriter
 import io/dynstream
+import io/poll
 import io/promise
 import io/serversocket
 import js/console
@@ -74,8 +74,8 @@ type
 func console(client: Client): Console {.jsfget.} =
   return client.consoleWrapper.console
 
-template selector(client: Client): Selector[int] =
-  client.pager.selector
+template pollData(client: Client): PollData =
+  client.pager.pollData
 
 template forkserver(client: Client): ForkServer =
   client.pager.forkserver
@@ -144,6 +144,10 @@ proc evalJS(client: Client; src, filename: string; module = false): JSValue =
 proc evalJSFree(client: Client; src, filename: string) =
   JS_FreeValue(client.jsctx, client.evalJS(src, filename))
 
+proc evalJSFree2(opaque: RootRef; src, filename: string) =
+  let client = Client(opaque)
+  client.evalJSFree(src, filename)
+
 proc command0(client: Client; src: string; filename = "<command>";
     silence = false; module = false) =
   let ret = client.evalJS(src, filename, module = module)
@@ -358,9 +362,6 @@ proc input(client: Client): EmptyPromise =
     p.resolve()
   return p
 
-when ioselSupportedPlatform:
-  let SIGWINCH {.importc, header: "<signal.h>", nodecl.}: cint
-
 proc showConsole(client: Client) {.jsfunc.} =
   let container = client.consoleWrapper.container
   if client.pager.container != container:
@@ -381,7 +382,7 @@ proc acceptBuffers(client: Client) =
     if container.iface != nil: # fully connected
       let stream = container.iface.stream
       let fd = int(stream.source.fd)
-      client.selector.unregister(fd)
+      client.pollData.unregister(fd)
       client.loader.unset(fd)
       stream.sclose()
     elif container.process != -1: # connecting to buffer process
@@ -390,12 +391,12 @@ proc acceptBuffers(client: Client) =
     elif (let item = pager.findConnectingContainer(container); item != nil):
       # connecting to URL
       let stream = item.stream
-      client.selector.unregister(int(stream.fd))
+      client.pollData.unregister(int(stream.fd))
       stream.sclose()
       client.loader.unset(item)
   let registerFun = proc(fd: int) =
-    client.selector.unregister(fd)
-    client.selector.registerHandle(fd, {Read, Write}, 0)
+    client.pollData.unregister(fd)
+    client.pollData.register(fd, POLLIN or POLLOUT)
   for item in pager.procmap:
     let container = item.container
     let stream = connectSocketStream(client.config.external.sockdir,
@@ -444,7 +445,7 @@ proc acceptBuffers(client: Client) =
       container.setCloneStream(stream, registerFun)
     let fd = int(stream.fd)
     client.loader.put(ContainerData(stream: stream, container: container))
-    client.selector.registerHandle(fd, {Read}, 0)
+    client.pollData.register(fd, POLLIN)
     pager.handleEvents(container)
   pager.procmap.setLen(0)
 
@@ -505,8 +506,8 @@ proc handleRead(client: Client; fd: int) =
 proc handleWrite(client: Client; fd: int) =
   let container = ContainerData(client.loader.get(fd)).container
   if container.iface.stream.flushWrite():
-    client.selector.unregister(fd)
-    client.selector.registerHandle(fd, {Read}, 0)
+    client.pollData.unregister(fd)
+    client.pollData.register(fd, POLLIN)
 
 proc flushConsole*(client: Client) {.jsfunc.} =
   if client.console == nil:
@@ -534,7 +535,7 @@ proc handleError(client: Client; fd: int) =
         client.console.error("Error in buffer", $container.url)
       else:
         client.consoleWrapper.container = nil
-      client.selector.unregister(fd)
+      client.pollData.unregister(fd)
       client.loader.unset(fd)
       doAssert client.consoleWrapper.container != nil
       client.showConsole()
@@ -546,31 +547,48 @@ proc handleError(client: Client; fd: int) =
     doAssert client.consoleWrapper.container != nil
     client.showConsole()
 
+let SIGWINCH {.importc, header: "<signal.h>", nodecl.}: cint
+
+proc setupSigwinch(client: Client): PosixStream =
+  var pipefd {.noinit.}: array[2, cint]
+  doAssert pipe(pipefd) != -1
+  let writer = newPosixStream(pipefd[1])
+  writer.setBlocking(false)
+  var gwriter {.global.}: PosixStream = nil
+  gwriter = writer
+  onSignal SIGWINCH:
+    discard sig
+    try:
+      gwriter.sendDataLoop([0u8])
+    except ErrorAgain:
+      discard
+  let reader = newPosixStream(pipefd[0])
+  reader.setBlocking(false)
+  return reader
+
 proc inputLoop(client: Client) =
-  let selector = client.selector
-  selector.registerHandle(int(client.pager.term.istream.fd), {Read}, 0)
-  when ioselSupportedPlatform:
-    let sigwinch = selector.registerSignal(int(SIGWINCH), 0)
-  var keys: array[64, ReadyKey]
+  client.pollData.register(client.pager.term.istream.fd, POLLIN)
+  let sigwinch = client.setupSigwinch()
+  client.pollData.register(sigwinch.fd, POLLIN)
   while true:
-    let count = client.selector.selectInto(-1, keys)
-    for event in keys.toOpenArray(0, count - 1):
-      if Read in event.events:
-        client.handleRead(event.fd)
-      if Write in event.events:
-        client.handleWrite(event.fd)
-      if Error in event.events:
-        client.handleError(event.fd)
-      when ioselSupportedPlatform:
-        if Signal in event.events:
-          assert event.fd == sigwinch
+    let timeout = client.timeouts.sortAndGetTimeout()
+    client.pollData.poll(timeout)
+    for event in client.pollData.events:
+      let efd = int(event.fd)
+      if (event.revents and POLLIN) != 0:
+        if event.fd == sigwinch.fd:
+          sigwinch.drain()
           client.pager.windowChange()
-      if selectors.Event.Timer in event.events:
-        let r = client.timeouts.runTimeoutFd(event.fd)
-        assert r
-        let container = client.consoleWrapper.container
-        if container != nil:
-          container.tailOnLoad = true
+        else:
+          client.handleRead(efd)
+      if (event.revents and POLLOUT) != 0:
+        client.handleWrite(efd)
+      if (event.revents and POLLERR) != 0 or (event.revents and POLLHUP) != 0:
+        client.handleError(efd)
+    if client.timeouts.run():
+      let container = client.consoleWrapper.container
+      if container != nil:
+        container.tailOnLoad = true
     client.runJSJobs()
     client.loader.unregistered.setLen(0)
     client.acceptBuffers()
@@ -604,19 +622,18 @@ func hasSelectFds(client: Client): bool =
     client.pager.procmap.len > 0
 
 proc headlessLoop(client: Client) =
-  var keys: array[64, ReadyKey]
   while client.hasSelectFds():
-    let count = client.selector.selectInto(-1, keys)
-    for event in keys.toOpenArray(0, count - 1):
-      if Read in event.events:
-        client.handleRead(event.fd)
-      if Write in event.events:
-        client.handleWrite(event.fd)
-      if Error in event.events:
-        client.handleError(event.fd)
-      if selectors.Event.Timer in event.events:
-        let r = client.timeouts.runTimeoutFd(event.fd)
-        assert r
+    let timeout = client.timeouts.sortAndGetTimeout()
+    client.pollData.poll(timeout)
+    for event in client.pollData.events:
+      let efd = int(event.fd)
+      if (event.revents and POLLIN) != 0:
+        client.handleRead(efd)
+      if (event.revents and POLLOUT) != 0:
+        client.handleWrite(efd)
+      if (event.revents and POLLERR) != 0 or (event.revents and POLLHUP) != 0:
+        client.handleError(efd)
+    discard client.timeouts.run()
     client.runJSJobs()
     client.loader.unregistered.setLen(0)
     client.acceptBuffers()
@@ -721,26 +738,25 @@ proc launchClient*(client: Client; pages: seq[string];
         dump = false
     else:
       dump = true
-  let selector = newSelector[int]()
-  let efd = int(client.forkserver.estream.fd)
-  selector.registerHandle(efd, {Read}, 0)
+  let pager = client.pager
+  pager.pollData.register(client.forkserver.estream.fd, POLLIN)
   client.loader.registerFun = proc(fd: int) =
-    selector.registerHandle(fd, {Read}, 0)
+    pager.pollData.register(fd, POLLIN)
   client.loader.unregisterFun = proc(fd: int) =
-    selector.unregister(fd)
-  client.pager.launchPager(istream, selector)
+    pager.pollData.unregister(fd)
+  pager.launchPager(istream)
   let clearFun = proc() =
     client.clearConsole()
   let showFun = proc() =
     client.showConsole()
   let hideFun = proc() =
     client.hideConsole()
-  client.consoleWrapper = client.pager.addConsole(interactive = istream != nil,
+  client.consoleWrapper = pager.addConsole(interactive = istream != nil,
     clearFun, showFun, hideFun)
   #TODO passing console.err here makes it impossible to change it later. maybe
   # better associate it with jsctx
-  client.timeouts = newTimeoutState(client.selector, client.jsctx,
-    client.console.err, proc(src, file: string) = client.evalJSFree(src, file))
+  client.timeouts = newTimeoutState(client.jsctx, client.console.err,
+    evalJSFree2, client)
   client.pager.timeouts = client.timeouts
   addExitProc((proc() = client.cleanup()))
   if client.config.start.startup_script != "":
diff --git a/src/local/pager.nim b/src/local/pager.nim
index 35f6ab4c..58ab9d77 100644
--- a/src/local/pager.nim
+++ b/src/local/pager.nim
@@ -4,7 +4,6 @@ import std/options
 import std/os
 import std/osproc
 import std/posix
-import std/selectors
 import std/sets
 import std/tables
 
@@ -14,6 +13,7 @@ import config/config
 import config/mailcap
 import io/bufreader
 import io/dynstream
+import io/poll
 import io/promise
 import io/stdio
 import io/tempfile
@@ -135,21 +135,21 @@ type
     jsctx: JSContext
     lastAlert: string # last alert seen by the user
     lineData: LineData
-    lineedit*: LineEdit
     lineHist: array[LineMode, LineHistory]
+    lineedit*: LineEdit
     linemode: LineMode
     loader*: FileLoader
     luctx: LUContext
     navDirection {.jsget.}: NavDirection
     notnum*: bool # has a non-numeric character been input already?
     numload*: int # number of pages currently being loaded
+    pollData*: PollData
     precnum*: int32 # current number prefix (when vi-numeric-prefix is true)
     procmap*: seq[ProcMapItem]
     refreshAllowed: HashSet[string]
     regex: Opt[Regex]
     reverseSearch: bool
     scommand*: string
-    selector*: Selector[int]
     status: Surface
     term*: Terminal
     timeouts*: TimeoutState
@@ -365,8 +365,7 @@ proc setLoader*(pager: Pager; loader: FileLoader) =
   )
   loader.key = pager.addLoaderClient(pager.loader.clientPid, config)
 
-proc launchPager*(pager: Pager; istream: PosixStream; selector: Selector[int]) =
-  pager.selector = selector
+proc launchPager*(pager: Pager; istream: PosixStream) =
   case pager.term.start(istream)
   of tsrSuccess: discard
   of tsrDA1Fail:
@@ -2045,7 +2044,7 @@ proc connected(pager: Pager; container: Container; response: Response) =
     pager.refreshStatusMsg()
 
 proc unregisterFd(pager: Pager; fd: int) =
-  pager.selector.unregister(fd)
+  pager.pollData.unregister(fd)
   pager.loader.unregistered.add(fd)
 
 proc handleRead*(pager: Pager; item: ConnectingContainer) =
diff --git a/src/server/buffer.nim b/src/server/buffer.nim
index af07f3f9..8adc3fd5 100644
--- a/src/server/buffer.nim
+++ b/src/server/buffer.nim
@@ -6,7 +6,6 @@ import std/net
 import std/options
 import std/os
 import std/posix
-import std/selectors
 import std/tables
 
 import chagashi/charset
@@ -29,6 +28,7 @@ import html/formdata as formdata_impl
 import io/bufreader
 import io/bufwriter
 import io/dynstream
+import io/poll
 import io/promise
 import io/serversocket
 import js/console
@@ -76,45 +76,44 @@ type
     y*: int
     str*: string
 
-  Buffer* = ref object
-    rfd: int # file descriptor of command pipe
+  Buffer = ref object
+    attrs: WindowAttributes
+    bgcolor: CellColor
+    bytesRead: int
+    cacheId: int
+    charset: Charset
+    charsetStack: seq[Charset]
+    config: BufferConfig
+    ctx: TextDecoderContext
+    document: Document
+    estream: DynFileStream # error stream
+    factory: CAtomFactory
     fd: int # file descriptor of buffer source
-    url: URL # URL before readFromFd
-    pstream: SocketStream # control stream
-    savetask: bool
-    ishtml: bool
     firstBufferRead: bool
-    lines: FlexibleGrid
+    hoverText: array[HoverType, string]
+    htmlParser: HTML5ParserWrapper
     images: seq[PosBitmap]
-    attrs: WindowAttributes
-    window: Window
-    document: Document
-    prevStyled: StyledNode
-    selector: Selector[int]
+    ishtml: bool
     istream: PosixStream
-    bytesRead: int
+    lines: FlexibleGrid
+    loader: FileLoader
+    needsBOMSniff: bool
+    outputId: int
+    pollData: PollData
+    prevStyled: StyledNode
+    prevnode: StyledNode
+    pstream: SocketStream # control stream
+    quirkstyle: CSSStylesheet
     reportedBytesRead: int
+    rfd: int # file descriptor of command pipe
+    savetask: bool
+    ssock: ServerSocket
     state: BufferState
-    prevnode: StyledNode
-    loader: FileLoader
-    config: BufferConfig
     tasks: array[BufferCommand, int] #TODO this should have arguments
-    hoverText: array[HoverType, string]
-    estream: DynFileStream # error stream
-    ssock: ServerSocket
-    factory: CAtomFactory
     uastyle: CSSStylesheet
-    quirkstyle: CSSStylesheet
+    url: URL # URL before readFromFd
     userstyle: CSSStylesheet
-    htmlParser: HTML5ParserWrapper
-    bgcolor: CellColor
-    needsBOMSniff: bool
-    ctx: TextDecoderContext
-    charsetStack: seq[Charset]
-    charset: Charset
-    cacheId: int
-    outputId: int
-    emptySel: Selector[int]
+    window: Window
 
   InterfaceOpaque = ref object
     stream: SocketStream
@@ -900,24 +899,16 @@ proc rewind(buffer: Buffer; offset: int; unregister = true): bool =
     return false
   buffer.loader.resume(response.outputId)
   if unregister:
-    buffer.selector.unregister(buffer.fd)
+    buffer.pollData.unregister(buffer.fd)
     buffer.loader.unregistered.add(buffer.fd)
   buffer.istream.sclose()
   buffer.istream = response.body
   buffer.istream.setBlocking(false)
   buffer.fd = response.body.fd
-  buffer.selector.registerHandle(buffer.fd, {Read}, 0)
+  buffer.pollData.register(buffer.fd, POLLIN)
   buffer.bytesRead = offset
   return true
 
-# As defined in std/selectors: this determines whether kqueue is being used.
-# On these platforms, we must not close the selector after fork, since kqueue
-# fds are not inherited after a fork.
-const bsdPlatform = defined(macosx) or defined(freebsd) or defined(netbsd) or
-  defined(openbsd) or defined(dragonfly)
-
-proc onload(buffer: Buffer)
-
 when defined(freebsd) or defined(openbsd):
   # necessary for an ugly hack we will do later
   import std/kqueue
@@ -951,33 +942,7 @@ proc clone*(buffer: Buffer; newurl: URL): int {.proxy.} =
     let sockFd = buffer.pstream.recvFileHandle()
     discard close(pipefd[0]) # close read
     let ps = newPosixStream(pipefd[1])
-    # We must allocate a new selector for this new process. (Otherwise we
-    # would interfere with operation of the other one.)
-    # Closing seems to suffice here.
-    when not bsdPlatform:
-      buffer.selector.close()
-    when defined(freebsd) or defined(openbsd):
-      # Hack necessary because newSelector calls sysctl, but Capsicum really
-      # dislikes that and we don't want to request sysctl capabilities
-      # from pledge either.
-      #
-      # To make this work we
-      # * allocate a new Selector object on buffer startup
-      # * copy into it the initial state of the real selector we will use
-      # * on fork, reset the selector object's state by writing the dummy
-      #   selector into it
-      # * override the file handle with a new kqueue().
-      #
-      # Warning: this breaks when threading is enabled; then fds is no longer a
-      # seq, so it's copied by reference (+ leaks). We explicitly disable
-      # threading, so for now we should be fine.
-      let fd = kqueue()
-      doAssert fd != -1
-      buffer.selector[] = buffer.emptySel[]
-      cast[ptr cint](buffer.selector)[] = fd
-    else:
-      buffer.selector = newSelector[int]()
-    #TODO set buffer.window.timeouts.selector
+    buffer.pollData.clear()
     var connecting: seq[ConnectData] = @[]
     var ongoing: seq[OngoingData] = @[]
     for it in buffer.loader.data:
@@ -1000,7 +965,7 @@ proc clone*(buffer: Buffer; newurl: URL): int {.proxy.} =
       response.body = stream
       let data = OngoingData(response: response, stream: stream)
       let fd = data.fd
-      buffer.selector.registerHandle(fd, {Read}, 0)
+      buffer.pollData.register(fd, POLLIN)
       buffer.loader.put(data)
     if buffer.istream != nil:
       # We do not own our input stream, so we can't tee it.
@@ -1025,7 +990,7 @@ proc clone*(buffer: Buffer; newurl: URL): int {.proxy.} =
     var r = buffer.pstream.initPacketReader()
     r.sread(buffer.loader.key)
     buffer.rfd = buffer.pstream.fd
-    buffer.selector.registerHandle(buffer.rfd, {Read}, 0)
+    buffer.pollData.register(buffer.rfd, POLLIN)
     # must reconnect after the new client is set up, or the client pids get
     # mixed up.
     for it in connecting:
@@ -1070,7 +1035,7 @@ proc finishLoad(buffer: Buffer): EmptyPromise =
   buffer.document.readyState = rsInteractive
   if buffer.config.scripting:
     buffer.dispatchDOMContentLoadedEvent()
-  buffer.selector.unregister(buffer.fd)
+  buffer.pollData.unregister(buffer.fd)
   buffer.loader.unregistered.add(buffer.fd)
   buffer.loader.removeCachedItem(buffer.cacheId)
   buffer.cacheId = -1
@@ -1181,12 +1146,12 @@ proc cancel*(buffer: Buffer) {.proxy.} =
     return
   for it in buffer.loader.data:
     let fd = it.fd
-    buffer.selector.unregister(fd)
+    buffer.pollData.unregister(fd)
     buffer.loader.unregistered.add(fd)
     it.stream.sclose()
     buffer.loader.unset(it)
   if buffer.istream != nil:
-    buffer.selector.unregister(buffer.fd)
+    buffer.pollData.unregister(buffer.fd)
     buffer.loader.unregistered.add(buffer.fd)
     buffer.loader.removeCachedItem(buffer.cacheId)
     buffer.fd = -1
@@ -1397,8 +1362,8 @@ proc click(buffer: Buffer; label: HTMLLabelElement): ClickResult =
 
 proc click(buffer: Buffer; select: HTMLSelectElement): ClickResult =
   let repaint = buffer.setFocus(select)
-  var options: seq[string]
-  var selected: seq[int]
+  var options: seq[string] = @[]
+  var selected: seq[int] = @[]
   var i = 0
   for option in select.options:
     options.add(option.textContent.stripAndCollapse())
@@ -1806,7 +1771,7 @@ proc handleRead(buffer: Buffer; fd: int): bool =
     assert false
   true
 
-proc handleError(buffer: Buffer; fd: int; err: OSErrorCode): bool =
+proc handleError(buffer: Buffer; fd: int): bool =
   if fd == buffer.rfd:
     # Connection reset by peer, probably. Close the buffer.
     return false
@@ -1815,32 +1780,36 @@ proc handleError(buffer: Buffer; fd: int; err: OSErrorCode): bool =
   elif buffer.loader.get(fd) != nil:
     if not buffer.loader.onError(fd):
       #TODO handle connection error
-      assert false, $fd & ": " & $err
+      assert false, $fd
     if buffer.config.scripting:
       buffer.window.runJSJobs()
   elif fd in buffer.loader.unregistered:
     discard # ignore
   else:
-    assert false, $fd & ": " & $err
+    assert false, $fd
   true
 
+proc getPollTimeout(buffer: Buffer): cint =
+  if not buffer.config.scripting:
+    return -1
+  return buffer.window.timeouts.sortAndGetTimeout()
+
 proc runBuffer(buffer: Buffer) =
   var alive = true
-  var keys: array[64, ReadyKey]
   while alive:
-    let count = buffer.selector.selectInto(-1, keys)
-    for event in keys.toOpenArray(0, count - 1):
-      if Read in event.events:
+    let timeout = buffer.getPollTimeout()
+    buffer.pollData.poll(timeout)
+    for event in buffer.pollData.events:
+      if (event.revents and POLLIN) != 0:
         if not buffer.handleRead(event.fd):
           alive = false
           break
-      if Error in event.events:
-        if not buffer.handleError(event.fd, event.errorCode):
+      if (event.revents and POLLERR) != 0 or (event.revents and POLLHUP) != 0:
+        if not buffer.handleError(event.fd):
           alive = false
           break
-      if selectors.Event.Timer in event.events:
-        let r = buffer.window.timeouts.runTimeoutFd(event.fd)
-        assert r
+    if buffer.config.scripting:
+      if buffer.window.timeouts.run():
         buffer.window.runJSJobs()
         buffer.maybeReshape()
     buffer.loader.unregistered.setLen(0)
@@ -1853,9 +1822,7 @@ proc cleanup(buffer: Buffer) =
 
 proc launchBuffer*(config: BufferConfig; url: URL; attrs: WindowAttributes;
     ishtml: bool; charsetStack: seq[Charset]; loader: FileLoader;
-    ssock: ServerSocket; pstream: SocketStream; selector: Selector[int]) =
-  let emptySel = Selector[int]()
-  emptySel[] = selector[]
+    ssock: ServerSocket; pstream: SocketStream) =
   let factory = newCAtomFactory()
   let confidence = if config.charsetOverride == CHARSET_UNKNOWN:
     ccTentative
@@ -1870,16 +1837,14 @@ proc launchBuffer*(config: BufferConfig; url: URL; attrs: WindowAttributes;
     needsBOMSniff: config.charsetOverride == CHARSET_UNKNOWN,
     pstream: pstream,
     rfd: pstream.fd,
-    selector: selector,
     ssock: ssock,
     url: url,
     charsetStack: charsetStack,
     cacheId: -1,
     outputId: -1,
-    emptySel: emptySel,
     factory: factory,
-    window: newWindow(config.scripting, config.images, config.styling, selector,
-      attrs, factory, loader, url)
+    window: newWindow(config.scripting, config.images, config.styling, attrs,
+      factory, loader, url)
   )
   if buffer.config.scripting:
     buffer.window.navigate = proc(url: URL) = buffer.navigate(url)
@@ -1891,12 +1856,12 @@ proc launchBuffer*(config: BufferConfig; url: URL; attrs: WindowAttributes;
   buffer.fd = int(fd)
   buffer.istream = newPosixStream(fd)
   buffer.istream.setBlocking(false)
-  buffer.selector.registerHandle(int(fd), {Read}, 0)
+  buffer.pollData.register(fd, POLLIN)
   loader.registerFun = proc(fd: int) =
-    buffer.selector.registerHandle(fd, {Read}, 0)
+    buffer.pollData.register(fd, POLLIN)
   loader.unregisterFun = proc(fd: int) =
-    buffer.selector.unregister(fd)
-  buffer.selector.registerHandle(buffer.rfd, {Read}, 0)
+    buffer.pollData.unregister(fd)
+  buffer.pollData.register(buffer.rfd, POLLIN)
   const css = staticRead"res/ua.css"
   const quirk = css & staticRead"res/quirk.css"
   buffer.initDecoder()
diff --git a/src/server/forkserver.nim b/src/server/forkserver.nim
index eaea5075..7c6997a8 100644
--- a/src/server/forkserver.nim
+++ b/src/server/forkserver.nim
@@ -1,7 +1,6 @@
 import std/options
 import std/os
 import std/posix
-import std/selectors
 import std/tables
 
 import chagashi/charset
@@ -149,10 +148,6 @@ proc forkBuffer(ctx: var ForkServerContext; r: var BufferedReader): int =
     discard close(pipefd[0]) # close read
     closeStdin()
     closeStdout()
-    # must call before entering the sandbox, or capsicum cries because of Nim
-    # calling sysctl
-    # also lets us deny sysctl call with pledge
-    let selector = newSelector[int]()
     setBufferProcessTitle(url)
     let pid = getCurrentProcessId()
     let ssock = initServerSocket(sockDir, sockDirFd, pid)
@@ -178,7 +173,7 @@ proc forkBuffer(ctx: var ForkServerContext; r: var BufferedReader): int =
     )
     try:
       launchBuffer(config, url, attrs, ishtml, charsetStack, loader,
-        ssock, pstream, selector)
+        ssock, pstream)
     except CatchableError:
       let e = getCurrentException()
       # taken from system/excpt.nim
diff --git a/src/utils/sandbox.nim b/src/utils/sandbox.nim
index 9e0498a5..f7afbb91 100644
--- a/src/utils/sandbox.nim
+++ b/src/utils/sandbox.nim
@@ -175,14 +175,10 @@ elif SandboxMode == stLibSeccomp:
       "clone", # for when fork is implemented as clone
       "close", # duh
       "connect", # for outgoing requests to loader
-      "epoll_create", "epoll_create1", "epoll_ctl", "epoll_wait", # epoll stuff
-      "epoll_pwait", # for bionic & musl
-      "eventfd", # used by Nim selectors
       "exit_group", # for quit
       "fork", # for when fork is really fork
       "futex", # bionic libc & WSL both need it
       "getpid", # for determining current PID after we fork
-      "getrlimit", # glibc uses it after fork it seems
       "getsockname", # Nim needs it for connecting
       "gettimeofday", # used by QuickJS in Date.now()
       "lseek", # glibc calls lseek on open files at exit
@@ -192,17 +188,12 @@ elif SandboxMode == stLibSeccomp:
       "munmap", # memory allocation
       "pipe", # for pipes to child process
       "pipe2", # for when pipe is implemented as pipe2
-      "prlimit64", # for when getrlimit is implemented as prlimit64
+      "poll", "ppoll", # for polling (sometimes implemented as ppoll, see musl)
       "read", "recv", "recvfrom", "recvmsg", # for reading from sockets
       "rt_sigreturn", # for when sigreturn is implemented as rt_sigreturn
       "send", "sendmsg", "sendto", # for writing to sockets
       "set_robust_list", # glibc seems to need it for whatever reason
-      "setrlimit", # glibc seems to use it for whatever reason
       "sigreturn", # called by signal trampoline
-      "timerfd_create", # used by Nim selectors
-      "timerfd_gettime", # not actually used by Nim but may be in the future
-      "timerfd_settime", # used by Nim selectors
-      "ugetrlimit", # glibc uses it after fork it seems
       "write" # for writing to sockets
     ]
     for it in allowList:
@@ -235,7 +226,7 @@ elif SandboxMode == stLibSeccomp:
       "read", "write", "recv", "send", "recvfrom", "sendto", # socket i/o
       "lseek", # glibc calls lseek on open files at exit
       "mmap", "mmap2", "mremap", "munmap", "brk", # memory allocation
-      "poll", # curl needs poll
+      "poll", "ppoll", # curl needs poll
       "getpid", # used indirectly by OpenSSL EVP_RAND_CTX_new (through drbg)
       "futex", # bionic libc & WSL both need it
       # we either have to use CURLOPT_NOSIGNAL or allow signals.