diff options
author | bptato <nincsnevem662@gmail.com> | 2024-03-28 01:36:29 +0100 |
---|---|---|
committer | bptato <nincsnevem662@gmail.com> | 2024-03-28 01:36:29 +0100 |
commit | b530ccc899a8cc8c63bad29abe1e479eb999b167 (patch) | |
tree | 07062947dfda3ac4356b0ce26de1cbe4e4c87ebd | |
parent | 52c415762fda7b9369ed4cf88783a6639574e3ea (diff) | |
download | chawan-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.nim | 2 | ||||
-rw-r--r-- | src/bindings/capsicum.nim | 6 | ||||
-rw-r--r-- | src/io/bind_unix.c | 16 | ||||
-rw-r--r-- | src/io/connect_unix.c | 16 | ||||
-rw-r--r-- | src/io/serversocket.nim | 38 | ||||
-rw-r--r-- | src/io/socketstream.nim | 30 | ||||
-rw-r--r-- | src/loader/loader.nim | 21 | ||||
-rw-r--r-- | src/local/client.nim | 7 | ||||
-rw-r--r-- | src/main.nim | 7 | ||||
-rw-r--r-- | src/server/buffer.nim | 20 | ||||
-rw-r--r-- | src/server/forkserver.nim | 33 | ||||
-rw-r--r-- | src/utils/sandbox.nim | 13 |
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 |