Subject: bug#45198: 28.0.50; Sandbox mode

From: Philipp Stephani <[email-masked]>
Date: Sat, 19 Dec 2020 19:18:48 +0100

Am Mo., 14. Dez. 2020 um 12:05 Uhr schrieb Philipp Stephani
<[email-masked]>:
>
> > >> - This will need someone else doing the implementation.
> > > Looks like we already have a volunteer for macOS.
> > > For Linux, this shouldn't be that difficult either. The sandbox needs
> > > to install a mount namespace that only allows read access to Emacs's
> > > installation directory plus any input file and write access to known
> > > output files, and enable syscall filters that forbid everything except
> > > a list of known-safe syscalls (especially exec). I can take a stab at
> > > that, but I can't promise anything ;-)
> >
> > Looking forward to it.
> >
>
> I've looked into this, and what I'd suggest for now is:
> 1. Add a --seccomp=FILE command-line option that loads seccomp filters
> from FILE and applies them directly after startup (first thing in
> main). Why do this in Emacs? Because that's the easiest way to prevent
> execve. When installing a seccomp filter in a separate process, execve
> needs to be allowed because otherwise there'd be no way to execute the
> Emacs binary. While there are workarounds (ptrace, LD_PRELOAD), it's
> easiest to install the seccomp filter directly in the Emacs process.

I've attached a patch for this.
From 80fc0ab92e1f025a8a41047d7909be6a97f5b09a Mon Sep 17 00:00:00 2001
From: Philipp Stephani <[email-masked]>
Date: Mon, 14 Dec 2020 21:25:11 +0100
Subject: [PATCH] Add support for --seccomp command-line option.

When passing this option on GNU/Linux, Emacs installs a Secure
Computing kernel system call filter.  See Bug#45198.

* configure.ac: Check for seccomp header.

* src/emacs.c (usage_message): Document --seccomp option.
(emacs_seccomp): New wrapper for 'seccomp' syscall.
(load_seccomp, maybe_load_seccomp, read_full): New helper functions.
(main): Potentially load seccomp filters during startup.
(standard_args): Add --seccomp option.

* lisp/startup.el (command-line): Detect and ignore --seccomp option.

* test/src/emacs-tests.el (emacs-tests/seccomp/absent-file)
(emacs-tests/seccomp/empty-file)
(emacs-tests/seccomp/invalid-file-size): New unit tests.
(emacs-tests--with-temp-file): New helper macro.

* etc/NEWS: Document new --seccomp option.
---
 configure.ac            |   2 +
 etc/NEWS                |  10 +++
 lisp/startup.el         |   4 +-
 src/emacs.c             | 168 +++++++++++++++++++++++++++++++++++++++-
 test/src/emacs-tests.el |  86 ++++++++++++++++++++
 5 files changed, 267 insertions(+), 3 deletions(-)
 create mode 100644 test/src/emacs-tests.el

diff --git a/configure.ac b/configure.ac
index 888b415148..087dd67e18 100644
--- a/configure.ac
+++ b/configure.ac
@@ -4184,6 +4184,8 @@ AC_DEFUN
 AC_SUBST([BLESSMAIL_TARGET])
 AC_SUBST([LIBS_MAIL])
 
+AC_CHECK_HEADERS([linux/seccomp.h])
+
 OLD_LIBS=$LIBS
 LIBS="$LIB_PTHREAD $LIB_MATH $LIBS"
 AC_CHECK_FUNCS(accept4 fchdir gethostname \
diff --git a/etc/NEWS b/etc/NEWS
index 4a8e70e6a6..bd255faca4 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -82,6 +82,16 @@ lacks the terminfo database, you can instruct Emacs to support 24-bit
 true color by setting 'COLORTERM=truecolor' in the environment.  This is
 useful on systems such as FreeBSD which ships only with "etc/termcap".
 
+** On GNU/Linux systems, Emacs now supports loading a Secure Computing
+filter.  To use this, you can pass a --seccomp=FILE command-line
+option to Emacs.  FILE must name a binary file containing an array of
+'struct sock_filter' structures.  Emacs will then install that list of
+Secure Computing filters into its own process early during the startup
+process.  You can use this functionality to put an Emacs process in a
+sandbox to avoid security issues when executing untrusted code.  See
+the manual page for 'seccomp' for details about Secure Computing
+filters.
+
 
 * Changes in Emacs 28.1
 
diff --git a/lisp/startup.el b/lisp/startup.el
index b1128f6d02..9b1346e448 100644
--- a/lisp/startup.el
+++ b/lisp/startup.el
@@ -1094,7 +1094,7 @@ command-line
                          ("--no-x-resources") ("--debug-init")
                          ("--user") ("--iconic") ("--icon-type") ("--quick")
 			 ("--no-blinking-cursor") ("--basic-display")
-                         ("--dump-file") ("--temacs")))
+                         ("--dump-file") ("--temacs") ("--seccomp")))
              (argi (pop args))
              (orig-argi argi)
              argval)
@@ -1146,7 +1146,7 @@ command-line
 	  (push '(visibility . icon) initial-frame-alist))
 	 ((member argi '("-nbc" "-no-blinking-cursor"))
 	  (setq no-blinking-cursor t))
-         ((member argi '("-dump-file" "-temacs"))  ; Handled in C
+         ((member argi '("-dump-file" "-temacs" "-seccomp"))  ; Handled in C
           (or argval (pop args))
           (setq argval nil))
 	 ;; Push the popped arg back on the list of arguments.
diff --git a/src/emacs.c b/src/emacs.c
index 2a32083ba1..0a939fc901 100644
--- a/src/emacs.c
+++ b/src/emacs.c
@@ -61,6 +61,13 @@ #define MAIN_PROGRAM
 # include <sys/socket.h>
 #endif
 
+#ifdef HAVE_LINUX_SECCOMP_H
+# include <linux/seccomp.h>
+# include <linux/filter.h>
+# include <sys/prctl.h>
+# include <sys/syscall.h>
+#endif
+
 #ifdef HAVE_WINDOW_SYSTEM
 #include TERM_HEADER
 #endif /* HAVE_WINDOW_SYSTEM */
@@ -239,6 +246,11 @@ #define MAIN_PROGRAM
     "\
 --dump-file FILE            read dumped state from FILE\n\
 ",
+#endif
+#ifdef HAVE_LINUX_SECCOMP_H
+    "\
+--sandbox=FILE              read Seccomp BPF filter from FILE\n\
+"
 #endif
     "\
 --no-build-details          do not add build details such as time stamps\n\
@@ -937,6 +949,150 @@ load_pdump (int argc, char **argv)
 }
 #endif /* HAVE_PDUMPER */
 
+#ifdef HAVE_LINUX_SECCOMP_H
+
+/* Wrapper function for the `seccomp' system call on GNU/Linux.  This system
+   call usually doesn't have a wrapper function.  See the manual page of
+   `seccomp' for the signature.  */
+
+static int
+emacs_seccomp (unsigned int operation, unsigned int flags, void *args)
+{
+#ifdef SYS_seccomp
+  return syscall (SYS_seccomp, operation, flags, args);
+#else
+  errno = ENOSYS;
+  return -1;
+#endif
+}
+
+/* Read exactly SIZE bytes into BUFFER.  Return false on short read.  */
+
+static bool
+read_full (int fd, void *buffer, size_t size)
+{
+  if (SSIZE_MAX < size)
+    {
+      errno = EFBIG;
+      return false;
+    }
+  char *ptr = buffer;
+  while (size != 0)
+    {
+      /* We can't use `emacs_read' yet because quitting doesn't work here
+         yet.  */
+      ssize_t ret = TEMP_FAILURE_RETRY (read (fd, ptr, size));
+      if (ret < 0)
+        return false;
+      if (ret == 0)
+        break;  /* Avoid infinite loop on encountering EOF.  */
+      eassert (ret <= size);
+      size -= ret;
+      ptr += ret;
+    }
+  if (size != 0)
+    errno = ENODATA;
+  return size == 0;
+}
+
+/* Attempt to load Secure Computing filters from FILE.  Return false if that
+   doesn't work for some reason.  */
+
+static bool
+load_seccomp (const char *file)
+{
+  bool success = false;
+  struct sock_fprog program = {0, NULL};
+  /* We can't use `emacs_open' yet because quitting doesn't work here yet.  */
+  int fd = TEMP_FAILURE_RETRY (open (file, O_RDONLY | O_BINARY | O_CLOEXEC));
+  if (fd < 0)
+    {
+      emacs_perror ("open");
+      goto out;
+    }
+  struct stat stat;
+  if (fstat (fd, &stat) < 0)
+    {
+      emacs_perror ("fstat");
+      goto out;
+    }
+  if (stat.st_size <= 0 || SIZE_MAX < stat.st_size
+      || (size_t) stat.st_size % sizeof *program.filter != 0)
+    {
+      fprintf (stderr, "seccomp filter %s has invalid size %jd\n", file,
+               (intmax_t) stat.st_size);
+      goto out;
+    }
+  size_t size = stat.st_size;
+  size_t count = size / sizeof *program.filter;
+  eassert (0 < size && 0 < count);
+  if (USHRT_MAX < count)
+    {
+      fprintf (stderr, "seccomp filter %s is too big", file);
+      goto out;
+    }
+  program.len = count;
+  program.filter = malloc (size);
+  if (program.filter == NULL)
+    {
+      emacs_perror ("malloc");
+      goto out;
+    }
+  if (!read_full (fd, program.filter, size))
+    {
+      emacs_perror ("read");
+      goto out;
+    }
+  if (close (fd) != 0)
+    emacs_perror ("close");  /* not critical */
+  fd = -1;
+
+  /* See man page of `seccomp' why this is necessary.  Note that we
+     intentionally don't check the return value: a parent process might have
+     made this call before, in which case it would fail; or, if enabling
+     privilege-restricting mode fails, the `seccomp' syscall will fail
+     anyway.  */
+  prctl (PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+  /* Install the filter.  Make sure that potential other threads can't escape
+     it.  */
+  if (emacs_seccomp (SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_TSYNC,
+                     &program)
+      != 0)
+    {
+      emacs_perror ("seccomp");
+      goto out;
+    }
+  success = true;
+
+ out:
+  free (program.filter);
+  close (fd);
+  return success;
+}
+
+/* Load Secure Computing filter from file specified with the --seccomp option.
+   Exit if that fails.  */
+
+static void
+maybe_load_seccomp (int argc, char **argv)
+{
+  int skip_args = 0;
+  char *file = NULL;
+  while (skip_args < argc - 1)
+    {
+      if (argmatch (argv, argc, "-seccomp", "--seccomp", 9, &file, &skip_args)
+          || argmatch (argv, argc, "--", NULL, 2, NULL, &skip_args))
+        break;
+      ++skip_args;
+    }
+  if (file == NULL)
+    return;
+  if (!load_seccomp (file))
+    fatal ("cannot enable seccomp filter from %s", file);
+}
+
+#endif  /* HAVE_LINUX_SECCOMP_H */
+
 int
 main (int argc, char **argv)
 {
@@ -944,6 +1100,13 @@ main (int argc, char **argv)
      for pointers.  */
   void *stack_bottom_variable;
 
+  /* First, check whether we should apply a seccomp filter.  This should come at
+     the very beginning to allow the filter to protect the initialization
+     phase.  */
+#ifdef HAVE_LINUX_SECCOMP_H
+  maybe_load_seccomp (argc, argv);
+#endif
+
   bool no_loadup = false;
   char *junk = 0;
   char *dname_arg = 0;
@@ -2137,12 +2300,15 @@ main (int argc, char **argv)
   { "-color", "--color", 5, 0},
   { "-no-splash", "--no-splash", 3, 0 },
   { "-no-desktop", "--no-desktop", 3, 0 },
-  /* The following two must be just above the file-name args, to get
+  /* The following three must be just above the file-name args, to get
      them out of our way, but without mixing them with file names.  */
   { "-temacs", "--temacs", 1, 1 },
 #ifdef HAVE_PDUMPER
   { "-dump-file", "--dump-file", 1, 1 },
 #endif
+#ifdef HAVE_LINUX_SECCOMP_H
+  { "-seccomp", "--seccomp", 1, 1 },
+#endif
 #ifdef HAVE_NS
   { "-NSAutoLaunch", 0, 5, 1 },
   { "-NXAutoLaunch", 0, 5, 1 },
diff --git a/test/src/emacs-tests.el b/test/src/emacs-tests.el
new file mode 100644
index 0000000000..279ecb210c
--- /dev/null
+++ b/test/src/emacs-tests.el
@@ -0,0 +1,86 @@
+;;; emacs-tests.el --- unit tests for emacs.c -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2020  Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Unit tests for src/emacs.c.
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'ert)
+
+(ert-deftest emacs-tests/seccomp/absent-file ()
+  (let ((emacs (expand-file-name invocation-name invocation-directory))
+        (process-environment nil))
+    (skip-unless (file-executable-p emacs))
+    (should-not (file-exists-p "/does-not-exist.bpf"))
+    (should-not
+     (eql (call-process emacs nil nil nil
+                        "--quick" "--batch" "--seccomp=/does-not-exist.bpf")
+          0))))
+
+(cl-defmacro emacs-tests--with-temp-file
+    (var (prefix &optional suffix text) &rest body)
+  "Evaluate BODY while a new temporary file exists.
+Bind VAR to the name of the file.  Pass PREFIX, SUFFIX, and TEXT
+to `make-temp-file', which see."
+  (declare (indent 2) (debug (symbolp (form form form) body)))
+  (cl-check-type var symbol)
+  ;; Use an uninterned symbol so that the code still works if BODY changes VAR.
+  (let ((filename (make-symbol "filename")))
+    `(let ((,filename (make-temp-file ,prefix nil ,suffix ,text)))
+       (unwind-protect
+           (let ((,var ,filename))
+             ,@body)
+         (delete-file ,filename)))))
+
+(ert-deftest emacs-tests/seccomp/empty-file ()
+  (let ((emacs (expand-file-name invocation-name invocation-directory))
+        (process-environment nil))
+    (skip-unless (file-executable-p emacs))
+    (emacs-tests--with-temp-file filter ("seccomp-invalid-" ".bpf")
+      ;; The --seccomp option is processed early, without filename handlers.
+      ;; Therefore remote or quoted filenames wouldn't work.
+      (should-not (file-remote-p filter))
+      (cl-callf file-name-unquote filter)
+      ;; According to the Seccomp man page, a filter must have at least one
+      ;; element, so Emacs should reject an empty file.
+      (should-not
+       (eql (call-process emacs nil nil nil
+                          "--quick" "--batch" (concat "--seccomp=" filter))
+            0)))))
+
+(ert-deftest emacs-tests/seccomp/invalid-file-size ()
+  (let ((emacs (expand-file-name invocation-name invocation-directory))
+        (process-environment nil))
+    (skip-unless (file-executable-p emacs))
+    (emacs-tests--with-temp-file filter ("seccomp-invalid-" ".bpf" "123456")
+      ;; The --seccomp option is processed early, without filename handlers.
+      ;; Therefore remote or quoted filenames wouldn't work.
+      (should-not (file-remote-p filter))
+      (cl-callf file-name-unquote filter)
+      ;; The Seccomp filter file must have a file size that's a multiple of the
+      ;; size of struct sock_filter, which is 8 or 16, but never 6.
+      (should-not
+       (eql (call-process emacs nil nil nil
+                          "--quick" "--batch" (concat "--seccomp=" filter))
+            0)))))
+
+;;; emacs-tests.el ends here
-- 
2.29.2.729.g45daf8777d-goog