emacs-diffs
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

scratch/seccomp-emacs-open be8328a: Add support for --seccomp command-li


From: Philipp Stephani
Subject: scratch/seccomp-emacs-open be8328a: Add support for --seccomp command-line option.
Date: Sat, 10 Apr 2021 12:58:24 -0400 (EDT)

branch: scratch/seccomp-emacs-open
commit be8328acf9aa464f848e682e63e417a18529af9e
Author: Philipp Stephani <phst@google.com>
Commit: Philipp Stephani <phst@google.com>

    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): 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/file-too-large)
    (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            |   5 +-
 etc/NEWS                |  10 +++
 lisp/startup.el         |   5 +-
 src/emacs.c             | 167 +++++++++++++++++++++++++++++++++++++++++++++++-
 test/src/emacs-tests.el | 131 +++++++++++++++++++++++++++++++++++++
 5 files changed, 314 insertions(+), 4 deletions(-)

diff --git a/configure.ac b/configure.ac
index 2c62a9f..684788a 100644
--- a/configure.ac
+++ b/configure.ac
@@ -4179,6 +4179,8 @@ fi
 AC_SUBST([BLESSMAIL_TARGET])
 AC_SUBST([LIBS_MAIL])
 
+AC_CHECK_HEADERS([linux/seccomp.h], [HAVE_SECCOMP=yes])
+
 OLD_LIBS=$LIBS
 LIBS="$LIB_PTHREAD $LIB_MATH $LIBS"
 AC_CHECK_FUNCS(accept4 fchdir gethostname \
@@ -5672,7 +5674,8 @@ optsep=
 emacs_config_features=
 for opt in ACL CAIRO DBUS FREETYPE GCONF GIF GLIB GMP GNUTLS GPM GSETTINGS \
  HARFBUZZ IMAGEMAGICK JPEG JSON LCMS2 LIBOTF LIBSELINUX LIBSYSTEMD LIBXML2 \
- M17N_FLT MODULES NOTIFY NS OLDXMENU PDUMPER PNG RSVG SOUND THREADS TIFF \
+ M17N_FLT MODULES NOTIFY NS OLDXMENU PDUMPER PNG RSVG SECCOMP SOUND \
+ THREADS TIFF \
  TOOLKIT_SCROLL_BARS UNEXEC X11 XAW3D XDBE XFT XIM XPM XWIDGETS X_TOOLKIT \
  ZLIB; do
 
diff --git a/etc/NEWS b/etc/NEWS
index 9ae3740..0956084 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -90,6 +90,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 b173d61..4d4c65e 100644
--- a/lisp/startup.el
+++ b/lisp/startup.el
@@ -1097,7 +1097,7 @@ please check its value")
                          ("--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)
@@ -1149,7 +1149,8 @@ please check its value")
          (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 fd08667..b956e9c 100644
--- a/src/emacs.c
+++ b/src/emacs.c
@@ -61,6 +61,13 @@ along with GNU Emacs.  If not, see 
<https://www.gnu.org/licenses/>.  */
 # 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 */
@@ -241,6 +248,11 @@ Initialization options:\n\
 --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\
 --no-desktop                do not load a saved desktop\n\
@@ -938,6 +950,149 @@ 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
+}
+
+/* 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;
+  void *buffer = NULL;
+  int fd
+    = emacs_open_noquit (file, O_RDONLY | O_CLOEXEC | O_BINARY, 0);
+  if (fd < 0)
+    {
+      emacs_perror ("open");
+      goto out;
+    }
+  struct stat stat;
+  if (fstat (fd, &stat) != 0)
+    {
+      emacs_perror ("fstat");
+      goto out;
+    }
+  if (! S_ISREG (stat.st_mode))
+    {
+      fprintf (stderr, "seccomp file %s is not regular\n", file);
+      goto out;
+    }
+  enum
+  {
+  /* See MAX_RW_COUNT in sysdep.c.  */
+#ifdef MAX_RW_COUNT
+    max_read_size = MAX_RW_COUNT
+#else
+    max_read_size = INT_MAX >> 18 << 18
+#endif
+  };
+  struct sock_fprog program;
+  if (stat.st_size <= 0 || SIZE_MAX <= stat.st_size
+      || PTRDIFF_MAX <= stat.st_size || max_read_size < stat.st_size
+      || stat.st_size % sizeof *program.filter != 0)
+    {
+      fprintf (stderr, "seccomp filter %s has invalid size %ld\n",
+               file, (long) stat.st_size);
+      goto out;
+    }
+  size_t size = stat.st_size;
+  size_t count = size / sizeof *program.filter;
+  eassert (0 < count && count < SIZE_MAX);
+  if (USHRT_MAX < count)
+    {
+      fprintf (stderr, "seccomp filter %s is too big\n", file);
+      goto out;
+    }
+  /* Try reading one more byte to detect file size changes.  */
+  buffer = malloc (size + 1);
+  if (buffer == NULL)
+    {
+      emacs_perror ("malloc");
+      goto out;
+    }
+  ptrdiff_t read = emacs_read (fd, buffer, size + 1);
+  if (read < 0)
+    {
+      emacs_perror ("read");
+      goto out;
+    }
+  if (read != count)
+    {
+      fprintf (stderr,
+               "seccomp filter %s changed size while reading\n",
+               file);
+      goto out;
+    }
+  if (emacs_close (fd) < 0)
+    emacs_perror ("close");  /* not a fatal error */
+  fd = -1;
+  program.len = count;
+  program.filter = buffer;
+
+  /* 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:
+  if (fd < 0)
+    emacs_close (fd);
+  free (buffer);
+  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)
 {
@@ -945,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;
@@ -2133,12 +2295,15 @@ static const struct standard_args standard_args[] =
   { "-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 0000000..7618a9c
--- /dev/null
+++ b/test/src/emacs-tests.el
@@ -0,0 +1,131 @@
+;;; 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)
+(require 'rx)
+
+(ert-deftest emacs-tests/seccomp/absent-file ()
+  (skip-unless (string-match-p (rx bow "SECCOMP" eow)
+                               system-configuration-features))
+  (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 ()
+  (skip-unless (string-match-p (rx bow "SECCOMP" eow)
+                               system-configuration-features))
+  (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/file-too-large ()
+  (skip-unless (string-match-p (rx bow "SECCOMP" eow)
+                               system-configuration-features))
+  (let ((emacs
+         (expand-file-name invocation-name invocation-directory))
+        (process-environment nil)
+        ;; This value should be correct on all supported systems.
+        (ushort-max #xFFFF)
+        ;; Either 8 or 16, but 16 should be large enough in all cases.
+        (filter-size 16))
+    (skip-unless (file-executable-p emacs))
+    (emacs-tests--with-temp-file
+        filter ("seccomp-too-large-" ".bpf"
+                (make-string (* (1+ ushort-max) filter-size) ?a))
+      ;; 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 filter count must fit into an `unsigned short'.  A bigger
+      ;; file should be rejected.
+      (should-not
+       (eql (call-process emacs nil nil nil
+                          "--quick" "--batch"
+                          (concat "--seccomp=" filter))
+            0)))))
+
+(ert-deftest emacs-tests/seccomp/invalid-file-size ()
+  (skip-unless (string-match-p (rx bow "SECCOMP" eow)
+                               system-configuration-features))
+  (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



reply via email to

[Prev in Thread] Current Thread [Next in Thread]