From 80fc0ab92e1f025a8a41047d7909be6a97f5b09a Mon Sep 17 00:00:00 2001 From: Philipp Stephani 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 #endif +#ifdef HAVE_LINUX_SECCOMP_H +# include +# include +# include +# include +#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 . + +;;; 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