about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2024-09-04 20:10:19 +0200
committerbptato <nincsnevem662@gmail.com>2024-09-04 20:42:35 +0200
commit412a78efc4eff3ea2b73f79cba8baee97cba9f18 (patch)
treeec65615bab94174038a1103a3143535e4f07fc81
parent9666c5a994830859a61078a1fabfef16b483c714 (diff)
downloadchawan-412a78efc4eff3ea2b73f79cba8baee97cba9f18.tar.gz
sixel, stbi, sandbox: fix fstat sandbox violation
Until recently, glibc used to implement it as fstatat. So don't trap
for fstatat (and for consistency, fstat), but return EPERM.

Just to be sure, rewrite sixel & stbi to never call fread.
-rw-r--r--Makefile4
-rw-r--r--adapter/img/sixel.nim87
-rw-r--r--adapter/img/stbi.nim7
-rw-r--r--src/bindings/libseccomp.nim3
-rw-r--r--src/utils/sandbox.nim25
5 files changed, 72 insertions, 54 deletions
diff --git a/Makefile b/Makefile
index 870382bd..9c4753fb 100644
--- a/Makefile
+++ b/Makefile
@@ -108,10 +108,10 @@ $(OUTDIR_CGI_BIN)/gopher: adapter/protocol/curlwrap.nim adapter/protocol/curlerr
 		adapter/gophertypes.nim adapter/protocol/curl.nim \
 		src/loader/connecterror.nim $(twtstr)
 $(OUTDIR_CGI_BIN)/stbi: adapter/img/stbi.nim adapter/img/stb_image.c \
-		adapter/img/stb_image.h src/utils/sandbox.nim
+		adapter/img/stb_image.h src/utils/sandbox.nim $(dynstream)
 $(OUTDIR_CGI_BIN)/jebp: adapter/img/jebp.c adapter/img/jebp.h \
 		src/utils/sandbox.nim
-$(OUTDIR_CGI_BIN)/sixel: src/types/color.nim src/utils/sandbox.nim $(twtstr)
+$(OUTDIR_CGI_BIN)/sixel: src/types/color.nim src/utils/sandbox.nim $(twtstr) $(dynstream)
 $(OUTDIR_CGI_BIN)/canvas: src/img/bitmap.nim src/img/painter.nim \
 	src/img/path.nim src/io/bufreader.nim src/types/color.nim \
 	src/types/line.nim src/utils/sandbox.nim $(dynstream) $(twtstr)
diff --git a/adapter/img/sixel.nim b/adapter/img/sixel.nim
index d34f8cf8..7f2cdb5d 100644
--- a/adapter/img/sixel.nim
+++ b/adapter/img/sixel.nim
@@ -20,26 +20,17 @@ import std/os
 import std/posix
 import std/strutils
 
+import io/dynstream
 import types/color
 import utils/sandbox
 import utils/twtstr
 
-const STDOUT_FILENO = 1
-
-proc writeAll(data: pointer; size: int) =
-  var n = 0
-  while n < size:
-    let i = write(STDOUT_FILENO, addr cast[ptr UncheckedArray[uint8]](data)[n],
-      int(size) - n)
-    assert i >= 0
-    n += i
-
-proc puts(s: string) =
-  if s.len > 0:
-    writeAll(unsafeAddr s[0], s.len)
+proc puts(os: PosixStream; s: string) =
+  os.sendDataLoop(s)
 
 proc die(s: string) {.noreturn.} =
-  puts(s)
+  let os = newPosixStream(STDOUT_FILENO)
+  os.puts(s)
   quit(1)
 
 const DCSSTART = "\eP"
@@ -147,18 +138,15 @@ proc trim(trimMap: var TrimMap; K: var int) =
   node.n = n
   K = k
 
-proc getPixel(s: string; m: int; bgcolor: ARGBColor): RGBColor {.inline.} =
-  let r = uint8(s[m])
-  let g = uint8(s[m + 1])
-  let b = uint8(s[m + 2])
-  let a = uint8(s[m + 3])
-  var c0 = RGBAColorBE(r: r, g: g, b: b, a: a)
+proc getPixel(img: seq[RGBAColorBE]; m: int; bgcolor: ARGBColor): RGBColor
+    {.inline.} =
+  var c0 = img[m]
   if c0.a != 255:
     let c1 = bgcolor.blend(c0)
     return RGBColor(uint32(rgb(c1.r, c1.g, c1.b)).fastmul(100))
   return RGBColor(uint32(rgb(c0.r, c0.g, c0.b)).fastmul(100))
 
-proc quantize(s: string; bgcolor: ARGBColor; palette: int): Node =
+proc quantize(img: seq[RGBAColorBE]; bgcolor: ARGBColor; palette: int): Node =
   let root = Node(leaf: false)
   # number of leaves
   var K = 0
@@ -168,9 +156,8 @@ proc quantize(s: string; bgcolor: ARGBColor; palette: int): Node =
   # batch together insertions of color runs
   var pc0 = RGBColor(0)
   var pcs = 0u32
-  for i in 0 ..< s.len div 4:
-    let m = i * 4
-    let c0 = s.getPixel(m, bgcolor)
+  for m in 0 ..< img.len:
+    let c0 = img.getPixel(m, bgcolor)
     if pc0 != c0:
       if pcs > 0:
         K += int(root.insert(pc0, trimMap, n = pcs))
@@ -347,13 +334,13 @@ proc createBands(bands: var seq[SixelBand]; chunkMap: seq[SixelChunk];
     if not found:
       bands.add(@[unsafeAddr chunk])
 
-proc encode(s: string; width, height, offx, offy, cropw: int; halfdump: bool;
-    bgcolor: ARGBColor; palette: int) =
+proc encode(img: seq[RGBAColorBE]; width, height, offx, offy, cropw: int;
+    halfdump: bool; bgcolor: ARGBColor; palette: int) =
   # reserve one entry for transparency
   # (this is necessary so that cropping works properly when the last
   # sixel would not fit on the screen, and also for images with !(height % 6).)
   let palette = palette - 1
-  let node = s.quantize(bgcolor, palette)
+  let node = img.quantize(bgcolor, palette)
   # prelude
   var outs = "Cha-Image-Dimensions: " & $width & 'x' & $height & "\n\n"
   let preludeLenPos = outs.len
@@ -368,13 +355,13 @@ proc encode(s: string; width, height, offx, offy, cropw: int; halfdump: bool;
     # prepend prelude size
     let L = outs.len - 4 - preludeLenPos # subtract length field
     outs.setU32BE(uint32(L), preludeLenPos)
-  puts(outs)
-  let W = width * 4
+  let os = newPosixStream(STDOUT_FILENO)
+  let W = width
   let H = W * height
   let realw = cropw - offx
   var n = offy * W
   var ymap = ""
-  var totalLen = 0
+  var totalLen = 0u32
   # add +2 so we don't have to bounds check
   var dither = Dither(
     d1: newSeq[DitherDiff](realw + 2),
@@ -382,17 +369,19 @@ proc encode(s: string; width, height, offx, offy, cropw: int; halfdump: bool;
   )
   var chunkMap = newSeq[SixelChunk](palette)
   var nrow = 1
+  # buffer to 64k, just because.
+  const MaxBuffer = 65546
   while true:
     if halfdump:
-      ymap.putU32BE(uint32(totalLen))
+      ymap.putU32BE(totalLen)
     for i in 0 ..< 6:
       if n >= H:
         break
       let mask = 1u8 shl i
       var chunk: ptr SixelChunk = nil
       for j in 0 ..< realw:
-        let m = n + (offx + j) * 4
-        let c0 = s.getPixel(m, bgcolor).correctDither(j, dither)
+        let m = n + offx + j
+        let c0 = img.getPixel(m, bgcolor).correctDither(j, dither)
         var diff: DitherDiff
         let c = node.getColor(c0, nodes, diff)
         dither.fs(j, diff)
@@ -424,27 +413,27 @@ proc encode(s: string; width, height, offx, offy, cropw: int; halfdump: bool;
       zeroMem(addr dither.d2[0], dither.d2.len * sizeof(dither.d2[0]))
     var bands: seq[SixelBand] = @[]
     bands.createBands(chunkMap, nrow)
-    outs.setLen(0)
-    for band in bands:
-      if outs.len > 0:
+    let olen = outs.len
+    for i in 0 ..< bands.len:
+      if i > 0:
         outs &= '$'
-      outs.compressSixel(band)
+      outs.compressSixel(bands[i])
     if n >= H:
       outs &= ST
-      totalLen += outs.len
+      totalLen += uint32(outs.len - olen)
       break
     else:
       outs &= '-'
-      totalLen += outs.len
-      puts(outs)
+      totalLen += uint32(outs.len - olen)
+      if outs.len >= MaxBuffer:
+        os.sendDataLoop(outs)
+        outs.setLen(0)
     inc nrow
   if halfdump:
-    ymap.putU32BE(uint32(totalLen))
+    ymap.putU32BE(totalLen)
     ymap.putU32BE(uint32(ymap.len))
     outs &= ymap
-    puts(outs)
-  else:
-    puts(outs)
+  os.sendDataLoop(outs)
 
 proc parseDimensions(s: string): (int, int) =
   let s = s.split('x')
@@ -512,9 +501,13 @@ proc main() =
       else:
         palette = 1024
     if width == 0 or height == 0:
-      puts("Cha-Image-Dimensions: 0x0\n")
+      let os = newPosixStream(STDOUT_FILENO)
+      os.sendDataLoop("Cha-Image-Dimensions: 0x0\n")
       quit(0) # done...
-    let s = stdin.readAll()
-    s.encode(width, height, offx, offy, cropw, halfdump, bgcolor, palette)
+    let n = width * height
+    var img = cast[seq[RGBAColorBE]](newSeqUninitialized[uint32](n))
+    let ps = newPosixStream(STDIN_FILENO)
+    ps.recvDataLoop(addr img[0], n * 4)
+    img.encode(width, height, offx, offy, cropw, halfdump, bgcolor, palette)
 
 main()
diff --git a/adapter/img/stbi.nim b/adapter/img/stbi.nim
index 8a1442f2..f0cfe2bb 100644
--- a/adapter/img/stbi.nim
+++ b/adapter/img/stbi.nim
@@ -3,6 +3,7 @@ import std/os
 import std/posix
 import std/strutils
 
+import io/dynstream
 import utils/sandbox
 import utils/twtstr
 
@@ -183,9 +184,9 @@ proc main() =
         if q < 1 or 100 < q:
           die("Cha-Control: ConnectionError 1 wrong quality")
         quality = cint(q)
-    let s = stdin.readAll()
-    if s.len != width * height * 4:
-      die("Cha-Control: ConnectionError 1 wrong size")
+    let ps = newPosixStream(STDIN_FILENO)
+    var s = newSeqUninitialized[uint8](width * height * 4)
+    ps.recvDataLoop(s)
     puts("Cha-Image-Dimensions: " & $width & 'x' & $height & "\n\n")
     let p = unsafeAddr s[0]
     case f
diff --git a/src/bindings/libseccomp.nim b/src/bindings/libseccomp.nim
index 81a6e969..3f02e4d9 100644
--- a/src/bindings/libseccomp.nim
+++ b/src/bindings/libseccomp.nim
@@ -37,6 +37,9 @@ const SCMP_ACT_KILL_PROCESS* = 0x80000000u32
 const SCMP_ACT_ALLOW* = 0x7FFF0000u32
 const SCMP_ACT_TRAP* = 0x00030000u32
 
+template SCMP_ACT_ERRNO*(x: uint16): uint32 =
+  0x50000u32 or x
+
 proc seccomp_init*(def_action: uint32): scmp_filter_ctx
 proc seccomp_reset*(ctx: scmp_filter_ctx; def_action: uint32): cint
 proc seccomp_syscall_resolve_name*(name: cstring): cint
diff --git a/src/utils/sandbox.nim b/src/utils/sandbox.nim
index 059bfe4b..a7168408 100644
--- a/src/utils/sandbox.nim
+++ b/src/utils/sandbox.nim
@@ -128,6 +128,27 @@ elif SandboxMode == stLibSeccomp:
         # PROT_WRITE (w/o PROT_READ) and PROT_NONE, which does no harm.
         doAssert seccomp_rule_add(ctx, SCMP_ACT_ALLOW, syscall, 1, arg2) == 0
 
+  proc blockStat(ctx: scmp_filter_ctx) =
+    # glibc calls fstat and its variants on fread, and it's quite hard
+    # to ensure we never use it. Plus, in older glibc versions (< 2.39),
+    # fstat is implemented as fstatat, and allowing that would imply
+    # access to arbitrary paths. So for consistency, we make all of them
+    # return an error.
+    #
+    # The offending function is _IO_file_doallocate; it doesn't actually
+    # look at errno, so EPERM should work fine.
+    const err = SCMP_ACT_ERRNO(uint16(EPERM))
+    const fstatList = [
+      cstring"fstat",
+      "fstat64",
+      "fstatat64",
+      "newfstatat",
+      "statx"
+    ]
+    for it in fstatList:
+      let syscall = seccomp_syscall_resolve_name(it)
+      doAssert seccomp_rule_add(ctx, err, syscall, 0) == 0
+
   proc enterBufferSandbox*(sockPath: string) =
     onSignal SIGSYS:
       discard sig
@@ -148,7 +169,6 @@ elif SandboxMode == stLibSeccomp:
       "exit_group", # for quit
       "fcntl", "fcntl64", # for changing blocking status
       "fork", # for when fork is really fork
-      "fstat", # glibc fread seems to call it
       "getpid", # for determining current PID after we fork
       "getrlimit", # glibc uses it after fork it seems
       "getsockname", # Nim needs it for connecting
@@ -185,6 +205,7 @@ elif SandboxMode == stLibSeccomp:
         datum_a: 1 # PF_LOCAL == PF_UNIX == AF_UNIX
       )
       doAssert seccomp_rule_add(ctx, SCMP_ACT_ALLOW, syscall, 1, arg0) == 0
+    ctx.blockStat()
     when defined(android):
       ctx.allowBionic()
     doAssert seccomp_load(ctx) == 0
@@ -204,7 +225,6 @@ elif SandboxMode == stLibSeccomp:
       "mmap", "mmap2", "mremap", "munmap", "brk", # memory allocation
       "poll", # curl needs poll
       "getpid", # used indirectly by OpenSSL EVP_RAND_CTX_new (through drbg)
-      "fstat", # glibc fread seems to call it
       # we either have to use CURLOPT_NOSIGNAL or allow signals.
       # do the latter, otherwise the default name resolver will never time out.
       "signal", "sigaction", "rt_sigaction",
@@ -212,6 +232,7 @@ elif SandboxMode == stLibSeccomp:
     for it in allowList:
       doAssert seccomp_rule_add(ctx, SCMP_ACT_ALLOW,
         seccomp_syscall_resolve_name(it), 0) == 0
+    ctx.blockStat()
     when defined(android):
       ctx.allowBionic()
     doAssert seccomp_load(ctx) == 0