unofficial mirror of bug-gnu-emacs@gnu.org 
 help / color / mirror / code / Atom feed
From: Philipp Stephani <p.stephani2@gmail.com>
To: Stefan Monnier <monnier@iro.umontreal.ca>
Cc: Bastien <bzg@gnu.org>,
	45198@debbugs.gnu.org, "João Távora" <joaotavora@gmail.com>
Subject: bug#45198: 28.0.50; Sandbox mode
Date: Sat, 19 Dec 2020 19:18:48 +0100	[thread overview]
Message-ID: <CAArVCkSiTR6UBT3AcKYOe3Yr8TgizjVBOobF_PDv0JNUySj2CA@mail.gmail.com> (raw)
In-Reply-To: <CAArVCkRahKpNVNQXsA_bYMoso-eQwy6b=LnaNTG9BtrJ0cMi1g@mail.gmail.com>

[-- Attachment #1: Type: text/plain, Size: 1260 bytes --]

Am Mo., 14. Dez. 2020 um 12:05 Uhr schrieb Philipp Stephani
<p.stephani2@gmail.com>:
>
> > >> - 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.

[-- Attachment #2: 0001-Add-support-for-seccomp-command-line-option.patch --]
[-- Type: text/x-patch, Size: 13272 bytes --]

From 80fc0ab92e1f025a8a41047d7909be6a97f5b09a Mon Sep 17 00:00:00 2001
From: Philipp Stephani <phst@google.com>
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.
+
 \f
 * 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


  parent reply	other threads:[~2020-12-19 18:18 UTC|newest]

Thread overview: 102+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2020-12-12 18:01 bug#45198: 28.0.50; Sandbox mode Stefan Monnier
2020-12-12 19:48 ` Eli Zaretskii
2020-12-12 21:06   ` Stefan Monnier
2020-12-13  3:29     ` Eli Zaretskii
2020-12-13  4:25       ` Stefan Monnier
2020-12-13 11:14         ` João Távora
2020-12-13 17:07         ` Philipp Stephani
2020-12-13 15:31 ` Mattias Engdegård
2020-12-13 17:09   ` Philipp Stephani
2020-12-13 17:04 ` Philipp Stephani
2020-12-13 17:57   ` Stefan Monnier
2020-12-13 18:13     ` Philipp Stephani
2020-12-13 18:43       ` Stefan Monnier
2020-12-14 11:05         ` Philipp Stephani
2020-12-14 14:44           ` Stefan Monnier
2020-12-14 15:37             ` Philipp Stephani
2020-12-19 22:41             ` Philipp Stephani
2020-12-19 23:16               ` Stefan Monnier
2020-12-20 12:28                 ` Philipp Stephani
2020-12-22 10:57                   ` Philipp Stephani
2020-12-22 14:43                     ` Stefan Monnier
2020-12-19 18:18           ` Philipp Stephani [this message]
2021-04-10 17:44             ` Philipp Stephani
2020-12-19 22:22           ` Philipp Stephani
2020-12-20 15:09             ` Eli Zaretskii
2020-12-20 18:14               ` Philipp Stephani
2020-12-20 18:29                 ` Eli Zaretskii
2020-12-20 18:39                   ` Philipp Stephani
2020-12-29 13:50             ` Philipp Stephani
2020-12-29 15:43               ` Eli Zaretskii
2020-12-29 16:05                 ` Philipp Stephani
2020-12-29 17:09                   ` Eli Zaretskii
2020-12-31 15:05                     ` Philipp Stephani
2020-12-31 16:50                       ` Eli Zaretskii
2021-04-10 19:11             ` Philipp Stephani
2020-12-13 18:52       ` Stefan Monnier
2020-12-13 20:13     ` João Távora
2020-12-14 11:12 ` Mattias Engdegård
2020-12-14 13:44   ` Philipp Stephani
2020-12-14 14:48     ` Stefan Monnier
2020-12-14 15:59     ` Mattias Engdegård
2020-12-17 13:08       ` Philipp Stephani
2020-12-17 17:55         ` Mattias Engdegård
2020-12-18 15:21           ` Philipp Stephani
2020-12-18 18:50             ` Mattias Engdegård
2020-12-19 15:08               ` Philipp Stephani
2020-12-19 17:19                 ` Mattias Engdegård
2020-12-19 18:11                   ` Stefan Monnier
2020-12-19 18:46                     ` Mattias Engdegård
2020-12-19 19:48                       ` João Távora
2020-12-19 21:01                       ` Stefan Monnier
2020-12-20 13:15                         ` Mattias Engdegård
2020-12-20 14:02                           ` Stefan Monnier
2020-12-20 14:12                             ` Mattias Engdegård
2020-12-20 15:08                               ` Stefan Monnier
2020-12-22 11:12                   ` Philipp Stephani
2020-12-28  8:23                     ` Stefan Kangas
2020-12-29 13:58                       ` Philipp Stephani
2020-12-30 14:59 ` Mattias Engdegård
2020-12-30 15:36   ` Alan Third
2021-04-17 15:26 ` Mattias Engdegård
2021-04-17 15:44   ` Philipp
2021-04-17 15:57     ` Eli Zaretskii
2021-04-17 16:10       ` Philipp
2021-04-17 16:15         ` Eli Zaretskii
2021-04-17 16:19           ` Eli Zaretskii
2021-04-17 16:20           ` Philipp Stephani
2021-04-17 16:33             ` Eli Zaretskii
2021-04-17 19:14               ` Philipp Stephani
2021-04-17 19:23                 ` Eli Zaretskii
2021-04-17 19:52                   ` Philipp
2021-04-18  6:20                     ` Eli Zaretskii
2021-04-18  9:11                       ` Philipp Stephani
2021-04-18  9:23                         ` Eli Zaretskii
2021-04-17 17:48         ` Mattias Engdegård
2021-04-17 18:21           ` Stefan Monnier
2021-04-17 18:59             ` Mattias Engdegård
2021-04-17 19:42               ` Philipp
2021-04-17 19:57                 ` Alan Third
2021-04-19 15:41                 ` Mattias Engdegård
2021-04-17 19:19           ` Philipp Stephani
2021-04-17 17:22     ` Mattias Engdegård
2021-04-17 17:57       ` Stefan Monnier
2021-04-17 19:21         ` Philipp Stephani
2021-04-17 19:16       ` Philipp Stephani
2021-04-17 16:58   ` Stefan Monnier
2021-04-17 17:14     ` Eli Zaretskii
2021-04-17 17:53       ` Stefan Monnier
2021-04-17 18:15         ` Eli Zaretskii
2021-04-17 18:47           ` Stefan Monnier
2021-04-17 19:14             ` Eli Zaretskii
2021-04-17 20:26               ` Stefan Monnier
2021-04-18  6:24                 ` Eli Zaretskii
2021-04-18 14:25                   ` Stefan Monnier
2021-07-05 19:12                     ` Philipp
2021-09-17 12:13 ` Mattias Engdegård
2021-09-17 13:20   ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2021-09-17 19:49     ` Mattias Engdegård
2022-09-11 11:28       ` Lars Ingebrigtsen
2022-09-13 12:37         ` mattiase
2022-09-13 12:53           ` João Távora
2022-09-13 13:02             ` João Távora

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

  List information: https://www.gnu.org/software/emacs/

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=CAArVCkSiTR6UBT3AcKYOe3Yr8TgizjVBOobF_PDv0JNUySj2CA@mail.gmail.com \
    --to=p.stephani2@gmail.com \
    --cc=45198@debbugs.gnu.org \
    --cc=bzg@gnu.org \
    --cc=joaotavora@gmail.com \
    --cc=monnier@iro.umontreal.ca \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
Code repositories for project(s) associated with this public inbox

	https://git.savannah.gnu.org/cgit/emacs.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).