From: Philipp Stephani <p.stephani2@gmail.com>
To: Stefan Monnier <monnier@iro.umontreal.ca>
Cc: Bastien <bzg@gnu.org>,
45198@debbugs.gnu.org, "João Távora" <joaotavora@gmail.com>
Subject: bug#45198: 28.0.50; Sandbox mode
Date: Sat, 19 Dec 2020 19:18:48 +0100 [thread overview]
Message-ID: <CAArVCkSiTR6UBT3AcKYOe3Yr8TgizjVBOobF_PDv0JNUySj2CA@mail.gmail.com> (raw)
In-Reply-To: <CAArVCkRahKpNVNQXsA_bYMoso-eQwy6b=LnaNTG9BtrJ0cMi1g@mail.gmail.com>
[-- Attachment #1: Type: text/plain, Size: 1260 bytes --]
Am Mo., 14. Dez. 2020 um 12:05 Uhr schrieb Philipp Stephani
<p.stephani2@gmail.com>:
>
> > >> - This will need someone else doing the implementation.
> > > Looks like we already have a volunteer for macOS.
> > > For Linux, this shouldn't be that difficult either. The sandbox needs
> > > to install a mount namespace that only allows read access to Emacs's
> > > installation directory plus any input file and write access to known
> > > output files, and enable syscall filters that forbid everything except
> > > a list of known-safe syscalls (especially exec). I can take a stab at
> > > that, but I can't promise anything ;-)
> >
> > Looking forward to it.
> >
>
> I've looked into this, and what I'd suggest for now is:
> 1. Add a --seccomp=FILE command-line option that loads seccomp filters
> from FILE and applies them directly after startup (first thing in
> main). Why do this in Emacs? Because that's the easiest way to prevent
> execve. When installing a seccomp filter in a separate process, execve
> needs to be allowed because otherwise there'd be no way to execute the
> Emacs binary. While there are workarounds (ptrace, LD_PRELOAD), it's
> easiest to install the seccomp filter directly in the Emacs process.
I've attached a patch for this.
[-- Attachment #2: 0001-Add-support-for-seccomp-command-line-option.patch --]
[-- Type: text/x-patch, Size: 13272 bytes --]
From 80fc0ab92e1f025a8a41047d7909be6a97f5b09a Mon Sep 17 00:00:00 2001
From: Philipp Stephani <phst@google.com>
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.
+
\f
* 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 <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 +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 <https://www.gnu.org/licenses/>.
+
+;;; 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
next prev parent reply other threads:[~2020-12-19 18:18 UTC|newest]
Thread overview: 102+ messages / expand[flat|nested] mbox.gz Atom feed top
2020-12-12 18:01 bug#45198: 28.0.50; Sandbox mode Stefan Monnier
2020-12-12 19:48 ` Eli Zaretskii
2020-12-12 21:06 ` Stefan Monnier
2020-12-13 3:29 ` Eli Zaretskii
2020-12-13 4:25 ` Stefan Monnier
2020-12-13 11:14 ` João Távora
2020-12-13 17:07 ` Philipp Stephani
2020-12-13 15:31 ` Mattias Engdegård
2020-12-13 17:09 ` Philipp Stephani
2020-12-13 17:04 ` Philipp Stephani
2020-12-13 17:57 ` Stefan Monnier
2020-12-13 18:13 ` Philipp Stephani
2020-12-13 18:43 ` Stefan Monnier
2020-12-14 11:05 ` Philipp Stephani
2020-12-14 14:44 ` Stefan Monnier
2020-12-14 15:37 ` Philipp Stephani
2020-12-19 22:41 ` Philipp Stephani
2020-12-19 23:16 ` Stefan Monnier
2020-12-20 12:28 ` Philipp Stephani
2020-12-22 10:57 ` Philipp Stephani
2020-12-22 14:43 ` Stefan Monnier
2020-12-19 18:18 ` Philipp Stephani [this message]
2021-04-10 17:44 ` Philipp Stephani
2020-12-19 22:22 ` Philipp Stephani
2020-12-20 15:09 ` Eli Zaretskii
2020-12-20 18:14 ` Philipp Stephani
2020-12-20 18:29 ` Eli Zaretskii
2020-12-20 18:39 ` Philipp Stephani
2020-12-29 13:50 ` Philipp Stephani
2020-12-29 15:43 ` Eli Zaretskii
2020-12-29 16:05 ` Philipp Stephani
2020-12-29 17:09 ` Eli Zaretskii
2020-12-31 15:05 ` Philipp Stephani
2020-12-31 16:50 ` Eli Zaretskii
2021-04-10 19:11 ` Philipp Stephani
2020-12-13 18:52 ` Stefan Monnier
2020-12-13 20:13 ` João Távora
2020-12-14 11:12 ` Mattias Engdegård
2020-12-14 13:44 ` Philipp Stephani
2020-12-14 14:48 ` Stefan Monnier
2020-12-14 15:59 ` Mattias Engdegård
2020-12-17 13:08 ` Philipp Stephani
2020-12-17 17:55 ` Mattias Engdegård
2020-12-18 15:21 ` Philipp Stephani
2020-12-18 18:50 ` Mattias Engdegård
2020-12-19 15:08 ` Philipp Stephani
2020-12-19 17:19 ` Mattias Engdegård
2020-12-19 18:11 ` Stefan Monnier
2020-12-19 18:46 ` Mattias Engdegård
2020-12-19 19:48 ` João Távora
2020-12-19 21:01 ` Stefan Monnier
2020-12-20 13:15 ` Mattias Engdegård
2020-12-20 14:02 ` Stefan Monnier
2020-12-20 14:12 ` Mattias Engdegård
2020-12-20 15:08 ` Stefan Monnier
2020-12-22 11:12 ` Philipp Stephani
2020-12-28 8:23 ` Stefan Kangas
2020-12-29 13:58 ` Philipp Stephani
2020-12-30 14:59 ` Mattias Engdegård
2020-12-30 15:36 ` Alan Third
2021-04-17 15:26 ` Mattias Engdegård
2021-04-17 15:44 ` Philipp
2021-04-17 15:57 ` Eli Zaretskii
2021-04-17 16:10 ` Philipp
2021-04-17 16:15 ` Eli Zaretskii
2021-04-17 16:19 ` Eli Zaretskii
2021-04-17 16:20 ` Philipp Stephani
2021-04-17 16:33 ` Eli Zaretskii
2021-04-17 19:14 ` Philipp Stephani
2021-04-17 19:23 ` Eli Zaretskii
2021-04-17 19:52 ` Philipp
2021-04-18 6:20 ` Eli Zaretskii
2021-04-18 9:11 ` Philipp Stephani
2021-04-18 9:23 ` Eli Zaretskii
2021-04-17 17:48 ` Mattias Engdegård
2021-04-17 18:21 ` Stefan Monnier
2021-04-17 18:59 ` Mattias Engdegård
2021-04-17 19:42 ` Philipp
2021-04-17 19:57 ` Alan Third
2021-04-19 15:41 ` Mattias Engdegård
2021-04-17 19:19 ` Philipp Stephani
2021-04-17 17:22 ` Mattias Engdegård
2021-04-17 17:57 ` Stefan Monnier
2021-04-17 19:21 ` Philipp Stephani
2021-04-17 19:16 ` Philipp Stephani
2021-04-17 16:58 ` Stefan Monnier
2021-04-17 17:14 ` Eli Zaretskii
2021-04-17 17:53 ` Stefan Monnier
2021-04-17 18:15 ` Eli Zaretskii
2021-04-17 18:47 ` Stefan Monnier
2021-04-17 19:14 ` Eli Zaretskii
2021-04-17 20:26 ` Stefan Monnier
2021-04-18 6:24 ` Eli Zaretskii
2021-04-18 14:25 ` Stefan Monnier
2021-07-05 19:12 ` Philipp
2021-09-17 12:13 ` Mattias Engdegård
2021-09-17 13:20 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2021-09-17 19:49 ` Mattias Engdegård
2022-09-11 11:28 ` Lars Ingebrigtsen
2022-09-13 12:37 ` mattiase
2022-09-13 12:53 ` João Távora
2022-09-13 13:02 ` João Távora
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
List information: https://www.gnu.org/software/emacs/
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=CAArVCkSiTR6UBT3AcKYOe3Yr8TgizjVBOobF_PDv0JNUySj2CA@mail.gmail.com \
--to=p.stephani2@gmail.com \
--cc=45198@debbugs.gnu.org \
--cc=bzg@gnu.org \
--cc=joaotavora@gmail.com \
--cc=monnier@iro.umontreal.ca \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
Code repositories for project(s) associated with this public inbox
https://git.savannah.gnu.org/cgit/emacs.git
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).