diff options
-rw-r--r-- | configure.ac | 5 | ||||
-rw-r--r-- | etc/NEWS | 10 | ||||
-rw-r--r-- | lisp/startup.el | 5 | ||||
-rw-r--r-- | src/emacs.c | 167 | ||||
-rw-r--r-- | test/src/emacs-tests.el | 131 |
5 files changed, 314 insertions, 4 deletions
diff --git a/configure.ac b/configure.ac index 2c62a9fe6f7..684788a4d33 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 @@ -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 b173d619733..4d4c65e6c41 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 fd08667f3fd..b956e9ca34b 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 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 |