about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2024-03-28 01:36:29 +0100
committerbptato <nincsnevem662@gmail.com>2024-03-28 01:36:29 +0100
commitb530ccc899a8cc8c63bad29abe1e479eb999b167 (patch)
tree07062947dfda3ac4356b0ce26de1cbe4e4c87ebd
parent52c415762fda7b9369ed4cf88783a6639574e3ea (diff)
downloadchawan-b530ccc899a8cc8c63bad29abe1e479eb999b167.tar.gz
Add capsicum support
It's the sandboxing system of FreeBSD. Quite pleasant to work with.

(Just trying to figure out the basics with this one before tackling the
abomination that is seccomp.)

Indeed, the only non-trivial part was getting newSelector to work with
Capsicum. Long story short it doesn't, so we use an ugly pointer cast +
assignment. But even that is stdlib's "fault", not Capsicum's.

This also gets rid of that ugly SocketPath global.
-rw-r--r--adapter/protocol/http.nim2
-rw-r--r--src/bindings/capsicum.nim6
-rw-r--r--src/io/bind_unix.c16
-rw-r--r--src/io/connect_unix.c16
-rw-r--r--src/io/serversocket.nim38
-rw-r--r--src/io/socketstream.nim30
-rw-r--r--src/loader/loader.nim21
-rw-r--r--src/local/client.nim7
-rw-r--r--src/main.nim7
-rw-r--r--src/server/buffer.nim20
-rw-r--r--src/server/forkserver.nim33
-rw-r--r--src/utils/sandbox.nim13
12 files changed, 171 insertions, 38 deletions
diff --git a/adapter/protocol/http.nim b/adapter/protocol/http.nim
index 25e39330..2bdd6858 100644
--- a/adapter/protocol/http.nim
+++ b/adapter/protocol/http.nim
@@ -4,6 +4,7 @@ else:
   import std/os
 import std/posix
 import std/strutils
+import utils/sandbox
 
 import curl
 import curlerrors
@@ -76,6 +77,7 @@ proc curlPreRequest(clientp: pointer, conn_primary_ip, conn_local_ip: cstring,
   let op = cast[HttpHandle](clientp)
   op.connectreport = true
   puts("Cha-Control: Connected\n")
+  enterSandbox()
   return 0 # ok
 
 proc main() =
diff --git a/src/bindings/capsicum.nim b/src/bindings/capsicum.nim
new file mode 100644
index 00000000..e01c3efb
--- /dev/null
+++ b/src/bindings/capsicum.nim
@@ -0,0 +1,6 @@
+{.push header: "sys/capsicum.h", importc.}
+
+proc cap_enter*(): cint
+proc cap_getmode*(modep: ptr cuint): cint
+
+{.pop.}
diff --git a/src/io/bind_unix.c b/src/io/bind_unix.c
index 02e86eed..398c2c9e 100644
--- a/src/io/bind_unix.c
+++ b/src/io/bind_unix.c
@@ -1,4 +1,5 @@
 #include <stddef.h>
+#include <sys/types.h>
 #include <sys/socket.h>
 #include <sys/un.h>
 
@@ -12,3 +13,18 @@ int bind_unix_from_c(int socket, const char *path, int pathlen)
 	memcpy(sa.sun_path, path, pathlen + 1);
 	return bind(socket, (struct sockaddr *)&sa, len);
 }
+
+#ifdef __FreeBSD__
+#include <fcntl.h>
+
+int bindat_unix_from_c(int fd, int socket, const char *path, int pathlen)
+{
+	struct sockaddr_un sa = {
+		.sun_family = AF_UNIX
+	};
+	int len = offsetof(struct sockaddr_un, sun_path) + pathlen + 1;
+
+	memcpy(sa.sun_path, path, pathlen + 1);
+	return bindat(fd, socket, (struct sockaddr *)&sa, len);
+}
+#endif
diff --git a/src/io/connect_unix.c b/src/io/connect_unix.c
index 26b3d6db..41faab11 100644
--- a/src/io/connect_unix.c
+++ b/src/io/connect_unix.c
@@ -1,4 +1,5 @@
 #include <stddef.h>
+#include <sys/types.h>
 #include <sys/socket.h>
 #include <sys/un.h>
 
@@ -12,3 +13,18 @@ int connect_unix_from_c(int socket, const char *path, int pathlen)
 	memcpy(sa.sun_path, path, pathlen + 1);
 	return connect(socket, (struct sockaddr *)&sa, len);
 }
+
+#ifdef __FreeBSD__
+#include <fcntl.h>
+
+int connectat_unix_from_c(int fd, int socket, const char *path, int pathlen)
+{
+	struct sockaddr_un sa = {
+		.sun_family = AF_UNIX
+	};
+	int len = offsetof(struct sockaddr_un, sun_path) + pathlen + 1;
+
+	memcpy(sa.sun_path, path, pathlen + 1);
+	return connectat(fd, socket, (struct sockaddr *)&sa, len);
+}
+#endif
diff --git a/src/io/serversocket.nim b/src/io/serversocket.nim
index a6acc555..ea5bc97d 100644
--- a/src/io/serversocket.nim
+++ b/src/io/serversocket.nim
@@ -9,26 +9,44 @@ type ServerSocket* = object
   sock*: Socket
   path*: string
 
-var SocketDirectory* = "/tmp/cha"
 const SocketPathPrefix = "cha_sock_"
-proc getSocketPath*(pid: int): string =
-  SocketDirectory / SocketPathPrefix & $pid
+proc getSocketName*(pid: int): string =
+  SocketPathPrefix & $pid
+
+proc getSocketPath*(socketDir: string; pid: int): string =
+  socketDir / getSocketName(pid)
 
 # The way stdlib does bindUnix is utterly broken at least on FreeBSD.
 # It seems that just writing it in C is the easiest solution.
 {.compile: "bind_unix.c".}
-proc bind_unix_from_c(fd: cint, path: cstring, pathlen: cint): cint {.importc.}
+proc bind_unix_from_c(fd: cint; path: cstring; pathlen: cint): cint
+  {.importc.}
+
+when defined(freebsd):
+  # capsicum stuff
+  proc unlinkat(dfd: cint; path: cstring; flag: cint): cint
+    {.importc, header: "<unistd.h>".}
+  proc bindat_unix_from_c(dfd, sock: cint; path: cstring; pathlen: cint): cint
+    {.importc.}
 
-proc initServerSocket*(pid: int; blocking = true): ServerSocket =
-  createDir(SocketDirectory)
+proc initServerSocket*(sockDir: string; sockDirFd, pid: int; blocking = true):
+    ServerSocket =
   let sock = newSocket(Domain.AF_UNIX, SockType.SOCK_STREAM,
     Protocol.IPPROTO_IP, buffered = false)
   if not blocking:
     sock.getFd().setBlocking(false)
-  let path = getSocketPath(pid)
-  discard unlink(cstring(path))
-  if bind_unix_from_c(cint(sock.getFd()), cstring(path), cint(path.len)) != 0:
-    raiseOSError(osLastError())
+  let path = getSocketPath(sockDir, pid)
+  if sockDirFd == -1:
+    discard unlink(cstring(path))
+    if bind_unix_from_c(cint(sock.getFd()), cstring(path), cint(path.len)) != 0:
+      raiseOSError(osLastError())
+  else:
+    when defined(freebsd):
+      let name = getSocketName(pid)
+      discard unlinkat(cint(sockDirFd), cstring(name), 0)
+      if bindat_unix_from_c(cint(sockDirFd), cint(sock.getFd()), cstring(name),
+          cint(name.len)) != 0:
+        raiseOSError(osLastError())
   listen(sock)
   return ServerSocket(sock: sock, path: path)
 
diff --git a/src/io/socketstream.nim b/src/io/socketstream.nim
index 3c8e6fa6..32aff96d 100644
--- a/src/io/socketstream.nim
+++ b/src/io/socketstream.nim
@@ -58,27 +58,41 @@ method sclose*(s: SocketStream) =
 
 # see serversocket.nim for an explanation
 {.compile: "connect_unix.c".}
-proc connect_unix_from_c(fd: cint, path: cstring, pathlen: cint): cint
+proc connect_unix_from_c(fd: cint; path: cstring; pathlen: cint): cint
   {.importc.}
+when defined(freebsd):
+  # for FreeBSD/capsicum
+  proc connectat_unix_from_c(baseFd, sockFd: cint; rel_path: cstring;
+    rel_pathlen: cint): cint {.importc.}
 
-proc connectSocketStream*(path: string; blocking = true): SocketStream =
+proc connectAtSocketStream0(socketDir: string; baseFd, pid: int;
+    blocking = true): SocketStream =
   let sock = newSocket(Domain.AF_UNIX, SockType.SOCK_STREAM,
     Protocol.IPPROTO_IP, buffered = false)
   if not blocking:
     sock.getFd().setBlocking(false)
-  if connect_unix_from_c(cint(sock.getFd()), cstring(path),
-      cint(path.len)) != 0:
-    raiseOSError(osLastError())
+  let path = getSocketPath(socketDir, pid)
+  if baseFd == -1:
+    if connect_unix_from_c(cint(sock.getFd()), cstring(path),
+        cint(path.len)) != 0:
+      raiseOSError(osLastError())
+  else:
+    when defined(freebsd):
+      doAssert baseFd != -1
+      let name = getSocketName(pid)
+      if connectat_unix_from_c(cint(baseFd), cint(sock.getFd()), cstring(name),
+          cint(name.len)) != 0:
+        raiseOSError(osLastError())
   return SocketStream(
     source: sock,
     fd: cint(sock.getFd()),
     blocking: blocking
   )
 
-proc connectSocketStream*(pid: int; blocking = true):
-    SocketStream =
+proc connectSocketStream*(socketDir: string; baseFd, pid: int;
+    blocking = true): SocketStream =
   try:
-    return connectSocketStream(getSocketPath(pid), blocking)
+    return connectAtSocketStream0(socketDir, baseFd, pid, blocking)
   except OSError:
     return nil
 
diff --git a/src/loader/loader.nim b/src/loader/loader.nim
index 295062b5..9f64b440 100644
--- a/src/loader/loader.nim
+++ b/src/loader/loader.nim
@@ -60,6 +60,10 @@ type
     unregistered*: seq[int]
     registerFun*: proc(fd: int)
     unregisterFun*: proc(fd: int)
+    # directory where we store UNIX domain sockets
+    sockDir*: string
+    # (FreeBSD only) fd for the socket directory so we can connectat() on it
+    sockDirFd*: int
 
   ConnectData = object
     promise: Promise[JSResult[Response]]
@@ -678,7 +682,8 @@ proc initLoaderContext(fd: cint; config: LoaderConfig): LoaderContext =
   )
   gctx = ctx
   let myPid = getCurrentProcessId()
-  ctx.ssock = initServerSocket(myPid, blocking = true)
+  # we don't capsicumize loader, so -1 is appropriate here
+  ctx.ssock = initServerSocket(config.tmpdir, -1, myPid, blocking = true)
   let sfd = int(ctx.ssock.sock.getFd())
   ctx.selector.registerHandle(sfd, {Read}, 0)
   # The server has been initialized, so the main process can resume execution.
@@ -847,7 +852,8 @@ template withLoaderPacketWriter(stream: SocketStream; loader: FileLoader;
     body
 
 proc connect(loader: FileLoader): SocketStream =
-  return connectSocketStream(loader.process, blocking = true)
+  return connectSocketStream(loader.sockDir, loader.sockDirFd, loader.process,
+    blocking = true)
 
 # Start a request. This should not block (not for a significant amount of time
 # anyway).
@@ -1092,3 +1098,14 @@ proc removeClient*(loader: FileLoader; pid: int) =
       w.swrite(lcRemoveClient)
       w.swrite(pid)
     stream.sclose()
+
+
+when defined(freebsd):
+  let O_DIRECTORY* {.importc, header: "<fcntl.h>", noinit.}: cint
+
+proc setSocketDir*(loader: FileLoader; path: string) =
+  loader.sockDir = path
+  when defined(freebsd):
+    loader.sockDirFd = open(cstring(path), O_DIRECTORY)
+  else:
+    loader.sockDirFd = -1
diff --git a/src/local/client.nim b/src/local/client.nim
index c63a18db..c4aad504 100644
--- a/src/local/client.nim
+++ b/src/local/client.nim
@@ -407,7 +407,8 @@ proc acceptBuffers(client: Client) =
     client.selector.registerHandle(fd, {Read, Write}, 0)
   for item in pager.procmap:
     let container = item.container
-    let stream = connectSocketStream(container.process)
+    let stream = connectSocketStream(client.config.external.tmpdir,
+      client.loader.sockDirFd, container.process)
     if stream == nil:
       pager.alert("Error: failed to set up buffer")
       continue
@@ -812,12 +813,14 @@ proc newClient*(config: Config; forkserver: ForkServer; jsctx: JSContext;
   let jsrt = JS_GetRuntime(jsctx)
   JS_SetModuleLoaderFunc(jsrt, normalizeModuleName, clientLoadJSModule, nil)
   let pager = newPager(config, forkserver, jsctx, warnings)
-  let loader = forkserver.newFileLoader(LoaderConfig(
+  let loaderPid = forkserver.forkLoader(LoaderConfig(
     urimethodmap: config.external.urimethodmap,
     w3mCGICompat: config.external.w3m_cgi_compat,
     cgiDir: seq[string](config.external.cgi_dir),
     tmpdir: config.external.tmpdir
   ))
+  let loader = FileLoader(process: loaderPid, clientPid: getCurrentProcessId())
+  loader.setSocketDir(config.external.tmpdir)
   pager.setLoader(loader)
   let client = Client(config: config, jsrt: jsrt, jsctx: jsctx, pager: pager)
   jsrt.setInterruptHandler(interruptHandler, cast[pointer](client))
diff --git a/src/main.nim b/src/main.nim
index 20d9c564..338e9518 100644
--- a/src/main.nim
+++ b/src/main.nim
@@ -1,13 +1,12 @@
 import version
 
 # Note: we can't just import std/os or the compiler cries. (No idea why.)
-from std/os import getEnv, putEnv, commandLineParams, getCurrentDir
+from std/os import getEnv, putEnv, commandLineParams, getCurrentDir, createDir
 import std/options
 
 import server/forkserver
 import config/chapath
 import config/config
-import io/serversocket
 import js/javascript
 import local/client
 import local/term
@@ -204,8 +203,10 @@ proc main() =
   if pages.len == 0 and not config.start.headless:
     if stdin.isatty():
       help(1)
+  # make sure tmpdir actually exists; if we do this later, then forkserver may
+  # try to open an empty dir
+  createDir(config.external.tmpdir)
   forkserver.loadForkServerConfig(config)
-  SocketDirectory = config.external.tmpdir
   let client = newClient(config, forkserver, jsctx, warnings)
   try:
     client.launchClient(pages, ctx.contentType, ctx.charset, ctx.dump)
diff --git a/src/server/buffer.nim b/src/server/buffer.nim
index 0df80567..12665334 100644
--- a/src/server/buffer.nim
+++ b/src/server/buffer.nim
@@ -904,6 +904,10 @@ const bsdPlatform = defined(macosx) or defined(freebsd) or defined(netbsd) or
 
 proc onload(buffer: Buffer)
 
+when defined(freebsd):
+  # necessary for an ugly hack we will do later
+  import std/kqueue
+
 # Create an exact clone of the current buffer.
 # This clone will share the loader process with the previous buffer.
 proc clone*(buffer: Buffer, newurl: URL): int {.proxy.} =
@@ -932,7 +936,14 @@ proc clone*(buffer: Buffer, newurl: URL): int {.proxy.} =
     # Closing seems to suffice here.
     when not bsdPlatform:
       buffer.selector.close()
-    buffer.selector = newSelector[int]()
+    when defined(freebsd):
+      # hack necessary because newSelector calls sysctl, but capsicum really
+      # dislikes that.
+      let fd = kqueue()
+      doAssert fd != -1
+      cast[ptr cint](buffer.selector)[] = fd
+    else:
+      buffer.selector = newSelector[int]()
     #TODO set buffer.window.timeouts.selector
     var cfds: seq[int] = @[]
     for fd in buffer.loader.connecting.keys:
@@ -964,7 +975,8 @@ proc clone*(buffer: Buffer, newurl: URL): int {.proxy.} =
       # We ignore errors; not much we can do with them here :/
       discard buffer.rewind(buffer.bytesRead, unregister = false)
     buffer.pstream.sclose()
-    let ssock = initServerSocket(myPid)
+    let ssock = initServerSocket(buffer.loader.sockDir, buffer.loader.sockDirFd,
+      myPid)
     buffer.ssock = ssock
     ps.write(char(0))
     buffer.url = newurl
@@ -1866,7 +1878,7 @@ proc cleanup(buffer: Buffer) =
 
 proc launchBuffer*(config: BufferConfig; url: URL; request: Request;
     attrs: WindowAttributes; ishtml: bool; charsetStack: seq[Charset];
-    loader: FileLoader; ssock: ServerSocket) =
+    loader: FileLoader; ssock: ServerSocket; selector: Selector[int]) =
   let pstream = ssock.acceptSocketStream()
   let buffer = Buffer(
     attrs: attrs,
@@ -1878,7 +1890,7 @@ proc launchBuffer*(config: BufferConfig; url: URL; request: Request;
     pstream: pstream,
     request: request,
     rfd: pstream.fd,
-    selector: newSelector[int](),
+    selector: selector,
     ssock: ssock,
     url: url,
     charsetStack: charsetStack,
diff --git a/src/server/forkserver.nim b/src/server/forkserver.nim
index 0e1f1d3b..c1bbdedb 100644
--- a/src/server/forkserver.nim
+++ b/src/server/forkserver.nim
@@ -1,6 +1,7 @@
 import std/options
 import std/os
 import std/posix
+import std/selectors
 import std/tables
 
 import config/config
@@ -16,6 +17,7 @@ import types/urimethodmap
 import types/url
 import types/winattrs
 import utils/proctitle
+import utils/sandbox
 import utils/strwidth
 
 import chagashi/charset
@@ -34,15 +36,17 @@ type
     ostream: PosixStream
     children: seq[int]
     loaderPid: int
+    sockDirFd: int
+    sockDir: string
 
-proc newFileLoader*(forkserver: ForkServer; config: LoaderConfig): FileLoader =
+proc forkLoader*(forkserver: ForkServer; config: LoaderConfig): int =
   forkserver.ostream.withPacketWriter w:
     w.swrite(fcForkLoader)
     w.swrite(config)
   var r = forkserver.istream.initPacketReader()
   var process: int
   r.sread(process)
-  return FileLoader(process: process, clientPid: getCurrentProcessId())
+  return process
 
 proc loadForkServerConfig*(forkserver: ForkServer, config: Config) =
   forkserver.ostream.withPacketWriter w:
@@ -137,10 +141,18 @@ proc forkBuffer(ctx: var ForkServerContext; r: var BufferedReader): int =
     for i in 0 ..< ctx.children.len: ctx.children[i] = 0
     ctx.children.setLen(0)
     let loaderPid = ctx.loaderPid
+    let sockDir = ctx.sockDir
+    let sockDirFd = ctx.sockDirFd
     zeroMem(addr ctx, sizeof(ctx))
     discard close(pipefd[0]) # close read
+    closeStdin()
+    closeStdout()
+    # must call before entering the sandbox, or capsicum cries because of Nim
+    # calling sysctl
+    let selector = newSelector[int]()
+    enterSandbox()
     let pid = getCurrentProcessId()
-    let ssock = initServerSocket(pid)
+    let ssock = initServerSocket(sockDir, sockDirFd, pid)
     gssock = ssock
     onSignal SIGTERM:
       # This will be overridden after buffer has been set up; it is only
@@ -150,16 +162,16 @@ proc forkBuffer(ctx: var ForkServerContext; r: var BufferedReader): int =
     let ps = newPosixStream(pipefd[1])
     ps.write(char(0))
     ps.sclose()
-    closeStdin()
-    closeStdout()
     let loader = FileLoader(
       process: loaderPid,
-      clientPid: pid
+      clientPid: pid,
+      sockDir: sockDir,
+      sockDirFd: sockDirFd
     )
     try:
       setBufferProcessTitle(url)
       launchBuffer(config, url, request, attrs, ishtml, charsetStack, loader,
-        ssock)
+        ssock, selector)
     except CatchableError:
       let e = getCurrentException()
       # taken from system/excpt.nim
@@ -180,7 +192,8 @@ proc runForkServer() =
   setProcessTitle("cha forkserver")
   var ctx = ForkServerContext(
     istream: newPosixStream(stdin.getFileHandle()),
-    ostream: newPosixStream(stdout.getFileHandle())
+    ostream: newPosixStream(stdout.getFileHandle()),
+    sockDirFd: -1
   )
   signal(SIGCHLD, SIG_IGN)
   while true:
@@ -212,7 +225,9 @@ proc runForkServer() =
           var config: ForkServerConfig
           r.sread(config)
           set_cjk_ambiguous(config.ambiguous_double)
-          SocketDirectory = config.tmpdir
+          ctx.sockDir = config.tmpdir
+          when defined(freebsd):
+            ctx.sockDirFd = open(cstring(ctx.sockDir), O_DIRECTORY)
     except EOFError:
       # EOF
       break
diff --git a/src/utils/sandbox.nim b/src/utils/sandbox.nim
new file mode 100644
index 00000000..88fc5c10
--- /dev/null
+++ b/src/utils/sandbox.nim
@@ -0,0 +1,13 @@
+when defined(freebsd):
+  import bindings/capsicum
+
+when defined(freebsd):
+  proc enterSandbox*() =
+    # per man:cap_enter(2), it may return ENOSYS if the kernel was compiled
+    # without CAPABILITY_MODE. So it seems better not to panic in this case.
+    # (But TODO: when we get enough sandboxing coverage it should print a
+    # warning or something.)
+    discard cap_enter()
+else:
+  proc enterSandbox*() =
+    discard