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.
This commit is contained in:
Philipp Stephani 2020-12-14 21:25:11 +01:00
parent a4eb3bd7d5
commit 2334f9bfa3
5 changed files with 266 additions and 4 deletions

View file

@ -4184,6 +4184,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 \
@ -5659,7 +5661,7 @@ emacs_config_features=
for opt in XAW3D XPM JPEG TIFF GIF PNG RSVG CAIRO IMAGEMAGICK SOUND GPM DBUS \
GCONF GSETTINGS GLIB NOTIFY ACL LIBSELINUX GNUTLS LIBXML2 FREETYPE HARFBUZZ M17N_FLT \
LIBOTF XFT ZLIB TOOLKIT_SCROLL_BARS X_TOOLKIT OLDXMENU X11 XDBE XIM \
NS MODULES THREADS XWIDGETS LIBSYSTEMD JSON PDUMPER UNEXEC LCMS2 GMP; do
NS MODULES THREADS XWIDGETS LIBSYSTEMD JSON PDUMPER UNEXEC LCMS2 GMP SECCOMP; do
case $opt in
PDUMPER) val=${with_pdumper} ;;

View file

@ -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

View file

@ -1094,7 +1094,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)
@ -1146,7 +1146,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.

View file

@ -33,6 +33,8 @@ along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
#include "lisp.h"
#include "sysstdio.h"
#include <read-file.h>
#ifdef WINDOWSNT
#include <fcntl.h>
#include <sys/socket.h>
@ -61,6 +63,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 */
@ -239,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\
@ -937,6 +951,100 @@ 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};
size_t size;
program.filter
= (struct sock_filter *) read_file (file, RF_BINARY, &size);
if (program.filter == NULL)
{
emacs_perror ("read_file");
goto out;
}
if (size == 0 || size % sizeof *program.filter != 0)
{
fprintf (stderr, "seccomp filter %s has invalid size %zu\n",
file, size);
goto out;
}
size_t count = size / sizeof *program.filter;
eassert (0 < size && 0 < count);
if (USHRT_MAX < count)
{
fprintf (stderr, "seccomp filter %s is too big\n", file);
goto out;
}
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:
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 +1052,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 +2252,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 },

131
test/src/emacs-tests.el Normal file
View file

@ -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