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:
parent
53dfd85a7f
commit
be8328acf9
5 changed files with 314 additions and 4 deletions
|
@ -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
|
||||
|
||||
|
|
10
etc/NEWS
10
etc/NEWS
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
167
src/emacs.c
167
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 */
|
||||
|
@ -240,6 +247,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\
|
||||
|
@ -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 },
|
||||
|
|
131
test/src/emacs-tests.el
Normal file
131
test/src/emacs-tests.el
Normal 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
|
Loading…
Add table
Reference in a new issue