summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPhilipp Stephani <phst@google.com>2020-12-14 21:25:11 +0100
committerPhilipp Stephani <phst@google.com>2021-01-17 15:50:10 +0100
commit6dec7327ab002903ac4791be33fec3669e6082c2 (patch)
tree198a4ea7d5d76a089beef1250fa66db4c14f0cb5
parent1773679af3241919a85d6174b1554070a63cca79 (diff)
downloademacs-6dec7327ab002903ac4791be33fec3669e6082c2.tar.gz
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.
-rw-r--r--configure.ac5
-rw-r--r--etc/NEWS10
-rw-r--r--lisp/startup.el5
-rw-r--r--src/emacs.c160
-rw-r--r--test/src/emacs-tests.el131
5 files changed, 307 insertions, 4 deletions
diff --git a/configure.ac b/configure.ac
index bea28338090..75ca5e7afa9 100644
--- a/configure.ac
+++ b/configure.ac
@@ -4189,6 +4189,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 \
@@ -5682,7 +5684,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 359d308bf19..a837f68517c 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 552802a38d7..e5028467e86 100644
--- a/lisp/startup.el
+++ b/lisp/startup.el
@@ -1095,7 +1095,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)
@@ -1147,7 +1147,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 77114271b27..7d6aa5cdba2 100644
--- a/src/emacs.c
+++ b/src/emacs.c
@@ -61,6 +61,14 @@ 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>
+# include <stdio.h>
+#endif
+
#ifdef HAVE_WINDOW_SYSTEM
#include TERM_HEADER
#endif /* HAVE_WINDOW_SYSTEM */
@@ -240,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\
@@ -937,6 +950,141 @@ 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;
+ struct sock_fprog program = {0, NULL};
+ int fd
+ = emacs_open_noquit (file, O_RDONLY | O_BINARY | O_CLOEXEC, 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 { element_size = sizeof *program.filter };
+ verify (0 < element_size && element_size < PTRDIFF_MAX);
+ if (stat.st_size <= 0 || SIZE_MAX <= stat.st_size
+ || PTRDIFF_MAX <= stat.st_size
+ || stat.st_size % element_size != 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 / element_size;
+ eassert (0 < count);
+ 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. */
+ program.filter = malloc (size + 1);
+ if (program.filter == NULL)
+ {
+ emacs_perror ("malloc");
+ goto out;
+ }
+ ptrdiff_t read = emacs_read (fd, program.filter, size + 1);
+ if (read < 0)
+ {
+ emacs_perror ("read");
+ goto out;
+ }
+ eassert (read <= SIZE_MAX);
+ if (read != size)
+ {
+ 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;
+
+ /* 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 (0 <= fd)
+ emacs_close (fd);
+ free (program.filter);
+ 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 +1092,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;
@@ -2132,12 +2287,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 00000000000..7618a9c6752
--- /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