Subject: bug#45198: 28.0.50; Sandbox mode

From: Philipp Stephani <[email-masked]>
Date: Sat, 19 Dec 2020 23:22:08 +0100

Am Mo., 14. Dez. 2020 um 12:05 Uhr schrieb Philipp Stephani
<[email-masked]>:

> > >> - 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:
> […]
> 2. Generate appropriate seccomp filters using libseccomp or similar.

Here's a patch for this step.
From f5d218ef354ec21d8566271570948d7c6b776649 Mon Sep 17 00:00:00 2001
From: Philipp Stephani <[email-masked]>
Date: Thu, 17 Dec 2020 11:20:55 +0100
Subject: [PATCH 2/2] Add a helper binary to create a basic Secure Computing
 filter.

The binary uses the 'seccomp' helper library.  The library isn't
needed to load the generated Secure Computing filter.

* configure.ac: Check for 'seccomp' header and library.

* lib-src/seccomp-filter.c: New helper binary to generate a generic
Secure Computing filter for GNU/Linux.

* lib-src/Makefile.in (DONT_INSTALL): Add 'seccomp-filter' helper
binary if possible.
(all): Add Secure Computing filter file if possible.
(seccomp-filter$(EXEEXT)): Compile helper binary.
(seccomp-filter.bpf seccomp-filter.pfc): Generate filter files.

* test/src/emacs-tests.el (emacs-tests/seccomp/allows-stdout)
(emacs-tests/seccomp/forbids-subprocess): New unit tests.

* test/Makefile.in (src/emacs-tests.log): Add dependency on the helper
binary.
---
 .gitignore                                  |   5 +
 configure.ac                                |   5 +
 lib-src/Makefile.in                         |  19 ++
 lib-src/seccomp-filter.c                    | 303 ++++++++++++++++++++
 test/Makefile.in                            |   2 +
 test/src/emacs-resources/seccomp-filter.bpf |   1 +
 test/src/emacs-tests.el                     |  44 +++
 7 files changed, 379 insertions(+)
 create mode 100644 lib-src/seccomp-filter.c
 create mode 120000 test/src/emacs-resources/seccomp-filter.bpf

diff --git a/.gitignore b/.gitignore
index bf7e934981..8c8b1f7584 100644
--- a/.gitignore
+++ b/.gitignore
@@ -187,6 +187,7 @@ lib-src/make-docfile
 lib-src/make-fingerprint
 lib-src/movemail
 lib-src/profile
+lib-src/seccomp-filter
 lib-src/test-distrib
 lib-src/update-game-score
 nextstep/Cocoa/Emacs.base/Contents/Info.plist
@@ -298,3 +299,7 @@ nt/emacs.rc
 nt/emacsclient.rc
 src/gdb.ini
 /var/
+
+# Seccomp filter files.
+lib-src/seccomp-filter.bpf
+lib-src/seccomp-filter.pfc
diff --git a/configure.ac b/configure.ac
index 087dd67e18..cb698e34fe 100644
--- a/configure.ac
+++ b/configure.ac
@@ -4186,6 +4186,11 @@ AC_DEFUN
 
 AC_CHECK_HEADERS([linux/seccomp.h])
 
+LIBSECCOMP=
+AC_CHECK_HEADER([seccomp.h],
+  [AC_CHECK_LIB([seccomp], [seccomp_init], [LIBSECCOMP=-lseccomp])])
+AC_SUBST([LIBSECCOMP])
+
 OLD_LIBS=$LIBS
 LIBS="$LIB_PTHREAD $LIB_MATH $LIBS"
 AC_CHECK_FUNCS(accept4 fchdir gethostname \
diff --git a/lib-src/Makefile.in b/lib-src/Makefile.in
index a2d27eab00..72a980e4de 100644
--- a/lib-src/Makefile.in
+++ b/lib-src/Makefile.in
@@ -209,6 +209,12 @@ LIB_EACCESS=
 ## empty or -lwsock2 for MinGW
 LIB_WSOCK32=@LIB_WSOCK32@
 
+LIBSECCOMP=@LIBSECCOMP@
+
+ifneq ($(LIBSECCOMP),)
+DONT_INSTALL += seccomp-filter$(EXEEXT)
+endif
+
 ## Extra libraries to use when linking movemail.
 LIBS_MOVE = $(LIBS_MAIL) $(KRB4LIB) $(DESLIB) $(KRB5LIB) $(CRYPTOLIB) \
             $(COM_ERRLIB) $(LIBHESIOD) $(LIBRESOLV) $(LIB_WSOCK32)
@@ -238,6 +244,10 @@ config_h =
 
 all: ${EXE_FILES} ${SCRIPTS}
 
+ifneq ($(LIBSECCOMP),)
+all: seccomp-filter.bpf
+endif
+
 .PHONY: all need-blessmail maybe-blessmail
 
 LOADLIBES = ../lib/libgnu.a $(LIBS_SYSTEM)
@@ -420,4 +430,13 @@ update-game-score${EXEEXT}:
 emacsclient.res: ../nt/emacsclient.rc $(NTINC)/../icons/emacs.ico
 	$(AM_V_RC)$(WINDRES) -O coff --include-dir=$(NTINC)/.. -o $@ $<
 
+ifneq ($(LIBSECCOMP),)
+seccomp-filter$(EXEEXT): $(srcdir)/seccomp-filter.c $(config_h)
+	$(AM_V_CCLD)$(CC) $(ALL_CFLAGS) $< $(LIBSECCOMP) -o $@
+
+seccomp-filter.bpf seccomp-filter.pfc: seccomp-filter$(EXEEXT)
+	$(AM_V_GEN)./seccomp-filter$(EXEEXT) \
+	  seccomp-filter.bpf seccomp-filter.pfc
+endif
+
 ## Makefile ends here.
diff --git a/lib-src/seccomp-filter.c b/lib-src/seccomp-filter.c
new file mode 100644
index 0000000000..e0fea0b421
--- /dev/null
+++ b/lib-src/seccomp-filter.c
@@ -0,0 +1,303 @@
+/* Generate a Secure Computing filter definition file.
+
+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/>.  */
+
+/* This program creates a small Secure Computing filter usable for a typical
+minimal Emacs sandbox.  See the man page for `seccomp' for details about Secure
+Computing filters.  This program requires the `libseccomp' library.  However,
+the resulting filter file requires only a Linux kernel supporting the Secure
+Computing extension.
+
+Usage:
+
+  seccomp-filter out.bpf out.pfc
+
+This writes the raw `struct sock_filter' array to out.bpf and a human-readable
+representation to out.pfc.  */
+
+#include "config.h"
+
+#include <errno.h>
+#include <limits.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <stdio.h>
+
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <sys/prctl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <linux/futex.h>
+#include <fcntl.h>
+#include <sched.h>
+#include <seccomp.h>
+#include <unistd.h>
+
+#include "verify.h"
+
+static ATTRIBUTE_FORMAT_PRINTF (2, 3) _Noreturn void
+fail (int error, const char *format, ...)
+{
+  va_list ap;
+  va_start (ap, format);
+  if (error == 0)
+    vfprintf (stderr, format, ap);
+  else
+    {
+      char buffer[1000];
+      vsnprintf (buffer, sizeof buffer, format, ap);
+      errno = error;
+      perror (buffer);
+    }
+  va_end (ap);
+  fflush (NULL);
+  exit (EXIT_FAILURE);
+}
+
+/* This binary is trivial, so we use a single global filter context object that
+   we release using `atexit'.  */
+
+static scmp_filter_ctx ctx;
+
+static void
+release_context (void)
+{
+  seccomp_release (ctx);
+}
+
+/* Wrapper functions and macros for libseccomp functions.  We exit immediately
+   upon any error to avoid error checking noise.  */
+
+static void
+set_attribute (enum scmp_filter_attr attr, uint32_t value)
+{
+  int status = seccomp_attr_set (ctx, attr, value);
+  if (status < 0)
+    fail (-status, "seccomp_attr_set (ctx, %u, %u)", attr, value);
+}
+
+/* Like `seccomp_rule_add (ACTION, SYSCALL, ...)', except that you don't have to
+   specify the number of comparator arguments, and any failure will exit the
+   process.  */
+
+#define RULE(action, syscall, ...)                                      \
+  do                                                                    \
+    {                                                                   \
+      const struct scmp_arg_cmp arg_array[] = {__VA_ARGS__};            \
+      enum { arg_cnt = sizeof arg_array / sizeof *arg_array };          \
+      int status = seccomp_rule_add_array (ctx, (action), (syscall),    \
+                                           arg_cnt, arg_array);         \
+      if (status < 0)                                                   \
+        fail (-status, "seccomp_rule_add_array (%s, %s, %d, {%s})",	\
+	      #action, #syscall, arg_cnt, #__VA_ARGS__);		\
+    }                                                                   \
+  while (false)
+
+static void
+export_filter (const char *file, int (*function) (const scmp_filter_ctx, int),
+               const char *name)
+{
+  int fd = TEMP_FAILURE_RETRY (
+    open (file, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY | O_CLOEXEC, 0644));
+  if (fd < 0)
+    fail (errno, "open %s", file);
+  int status = function (ctx, fd);
+  if (status < 0)
+    fail (-status, "%s", name);
+  if (close (fd) != 0)
+    fail (errno, "close");
+}
+
+#define EXPORT_FILTER(file, function) \
+  export_filter ((file), (function), #function)
+
+int
+main (int argc, char **argv)
+{
+  if (argc != 3)
+    fail (0, "usage: %s out.bpf out.pfc", argv[0]);
+
+  /* Any unhandled syscall should abort the Emacs process.  */
+  ctx = seccomp_init (SCMP_ACT_KILL_PROCESS);
+  if (ctx == NULL)
+    fail (0, "seccomp_init");
+  atexit (release_context);
+
+  /* We want to abort immediately if the architecture is unknown.  */
+  set_attribute (SCMP_FLTATR_ACT_BADARCH, SCMP_ACT_KILL_PROCESS);
+  set_attribute (SCMP_FLTATR_CTL_NNP, 1);
+  set_attribute (SCMP_FLTATR_CTL_TSYNC, 1);
+  set_attribute (SCMP_FLTATR_CTL_LOG, 0);
+
+  verify (CHAR_BIT == 8);
+  verify (sizeof (int) == 4 && INT_MIN == INT32_MIN && INT_MAX == INT32_MAX);
+  verify (sizeof (void *) == 8);
+  verify ((uintptr_t) NULL == 0);
+
+  /* Allow a clean exit.  */
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (exit));
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (exit_group));
+
+  /* Allow `mmap' and friends.  This is necessary for dynamic loading, reading
+     the portable dump file, and thread creation.  We don't allow pages to be
+     both writable and executable.  */
+  verify (MAP_PRIVATE != 0);
+  verify (MAP_SHARED != 0);
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (mmap),
+        SCMP_A2_32 (SCMP_CMP_MASKED_EQ, ~(PROT_NONE | PROT_READ | PROT_WRITE)),
+        /* Only support known flags.  MAP_DENYWRITE is ignored, but some
+           versions of the dynamic loader still use it.  Also allow allocating
+           thread stacks.  */
+        SCMP_A3_32 (SCMP_CMP_MASKED_EQ,
+                    ~(MAP_PRIVATE | MAP_FILE | MAP_ANONYMOUS | MAP_FIXED
+                      | MAP_DENYWRITE | MAP_STACK | MAP_NORESERVE),
+                    0));
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (mmap),
+        SCMP_A2_32 (SCMP_CMP_MASKED_EQ, ~(PROT_NONE | PROT_READ | PROT_EXEC)),
+        /* Only support known flags.  MAP_DENYWRITE is ignored, but some
+           versions of the dynamic loader still use it. */
+        SCMP_A3_32 (SCMP_CMP_MASKED_EQ,
+                    ~(MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED | MAP_DENYWRITE),
+                    0));
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (munmap));
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (mprotect),
+	/* Don't allow making pages executable.  */
+        SCMP_A2_32 (SCMP_CMP_MASKED_EQ, ~(PROT_NONE | PROT_READ | PROT_WRITE),
+                    0));
+
+  /* Futexes are used everywhere.  */
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (futex),
+        SCMP_A1_32 (SCMP_CMP_EQ, FUTEX_WAKE_PRIVATE));
+
+  /* Allow basic dynamic memory management.  */
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (brk));
+
+  /* Allow some status inquiries.  */
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (uname));
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (getuid));
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (geteuid));
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (getpid));
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (getpgrp));
+
+  /* Allow operations on open file descriptors.  File descriptors are
+     capabilities, and operating on them shouldn't cause security issues.  */
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (read));
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (write));
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (close));
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (lseek));
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (dup));
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (dup2));
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (fstat));
+
+  /* Allow read operations on the filesystem.  If necessary, these should be
+     further restricted using mount namespaces.  */
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (access));
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (faccessat));
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (stat));
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (stat64));
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (lstat));
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (lstat64));
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (fstatat64));
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (newfstatat));
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (readlink));
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (readlinkat));
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (getcwd));
+
+  /* Allow opening files, assuming they are only opened for reading.  */
+  verify (O_WRONLY != 0);
+  verify (O_RDWR != 0);
+  verify (O_CREAT != 0);
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (open),
+        SCMP_A1_32 (SCMP_CMP_MASKED_EQ,
+                    ~(O_RDONLY | O_BINARY | O_CLOEXEC | O_PATH | O_DIRECTORY),
+                    0));
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (openat),
+        SCMP_A2_32 (SCMP_CMP_MASKED_EQ,
+                    ~(O_RDONLY | O_BINARY | O_CLOEXEC | O_PATH | O_DIRECTORY),
+                    0));
+
+  /* Allow `tcgetpgrp'.  */
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (ioctl), SCMP_A0_32 (SCMP_CMP_EQ, STDIN_FILENO),
+        SCMP_A1_32 (SCMP_CMP_EQ, TIOCGPGRP));
+
+  /* Allow reading (but not setting) file flags.  */
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (fcntl), SCMP_A1_32 (SCMP_CMP_EQ, F_GETFL));
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (fcntl64), SCMP_A1_32 (SCMP_CMP_EQ, F_GETFL));
+
+  /* Allow reading random numbers from the kernel.  */
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (getrandom));
+
+  /* Changing the umask is uncritical.  */
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (umask));
+
+  /* Allow creation of pipes.  */
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (pipe));
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (pipe2));
+
+  /* Allow reading (but not changing) resource limits.  */
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (getrlimit));
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (prlimit64),
+	SCMP_A0_32 (SCMP_CMP_EQ, 0) /* pid == 0 (current process) */,
+        SCMP_A2_64 (SCMP_CMP_EQ, 0) /* new_limit == NULL */);
+
+  /* Block changing resource limits, but don't crash.  */
+  RULE (SCMP_ACT_ERRNO (EPERM), SCMP_SYS (prlimit64),
+        SCMP_A0_32 (SCMP_CMP_EQ, 0) /* pid == 0 (current process) */,
+        SCMP_A2_64 (SCMP_CMP_NE, 0) /* new_limit != NULL */);
+
+  /* Emacs installs signal handlers, which is harmless.  */
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (sigaction));
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (rt_sigaction));
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (sigprocmask));
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (rt_sigprocmask));
+
+  /* Allow timer support.  */
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (timer_create));
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (timerfd_create));
+
+  /* Allow thread creation.  See the NOTES section in the manual page for the
+     `clone' function.  */
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (clone),
+        SCMP_A0_64 (SCMP_CMP_MASKED_EQ,
+		    /* Flags needed to create threads.  See create_thread in
+		       libc.  */
+                    ~(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SYSVSEM
+                      | CLONE_SIGHAND | CLONE_THREAD | CLONE_SETTLS
+                      | CLONE_PARENT_SETTID | CLONE_CHILD_CLEARTID),
+                    0));
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (sigaltstack));
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (set_robust_list));
+
+  /* Allow setting the process name for new threads.  */
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (prctl), SCMP_A0_32 (SCMP_CMP_EQ, PR_SET_NAME));
+
+  /* Allow some event handling functions used by glib.  */
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (eventfd));
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (eventfd2));
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (wait4));
+  RULE (SCMP_ACT_ALLOW, SCMP_SYS (poll));
+
+  /* Don't allow creating sockets (network access would be extremely dangerous),
+     but also don't crash.  */
+  RULE (SCMP_ACT_ERRNO (EACCES), SCMP_SYS (socket));
+
+  EXPORT_FILTER (argv[1], seccomp_export_bpf);
+  EXPORT_FILTER (argv[2], seccomp_export_pfc);
+}
diff --git a/test/Makefile.in b/test/Makefile.in
index 67d203df29..99f69f7fcd 100644
--- a/test/Makefile.in
+++ b/test/Makefile.in
@@ -271,6 +271,8 @@ $(test_module): $(test_module:
 	  $(srcdir)/../lib/timespec.c $(srcdir)/../lib/gettime.c
 endif
 
+src/emacs-tests.log: ../lib-src/seccomp-filter.c
+
 ## Check that there is no 'automated' subdirectory, which would
 ## indicate an incomplete merge from an older version of Emacs where
 ## the tests were arranged differently.
diff --git a/test/src/emacs-resources/seccomp-filter.bpf b/test/src/emacs-resources/seccomp-filter.bpf
new file mode 120000
index 0000000000..b3d603d0ae
--- /dev/null
+++ b/test/src/emacs-resources/seccomp-filter.bpf
@@ -0,0 +1 @@
+../../../lib-src/seccomp-filter.bpf
\ No newline at end of file
diff --git a/test/src/emacs-tests.el b/test/src/emacs-tests.el
index 279ecb210c..9599c26783 100644
--- a/test/src/emacs-tests.el
+++ b/test/src/emacs-tests.el
@@ -25,6 +25,8 @@
 
 (require 'cl-lib)
 (require 'ert)
+(require 'ert-x)
+(require 'subr-x)
 
 (ert-deftest emacs-tests/seccomp/absent-file ()
   (let ((emacs (expand-file-name invocation-name invocation-directory))
@@ -83,4 +85,46 @@ emacs-tests/seccomp/invalid-file-size
                           "--quick" "--batch" (concat "--seccomp=" filter))
             0)))))
 
+(ert-deftest emacs-tests/seccomp/allows-stdout ()
+  (let ((emacs (expand-file-name invocation-name invocation-directory))
+        (filter (ert-resource-file "seccomp-filter.bpf"))
+        (process-environment nil))
+    (skip-unless (file-executable-p emacs))
+    (skip-unless (file-readable-p filter))
+    ;; 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)
+    (with-temp-buffer
+      (let ((status (call-process emacs nil t nil
+                                  "--quick" "--batch"
+                                  (concat "--seccomp=" filter)
+                                  (concat "--eval=" (prin1-to-string
+                                                     '(message "Hi"))))))
+        (ert-info ((format "Process output: %s" (buffer-string)))
+          (should (eql status 0)))
+        (should (equal (string-trim (buffer-string)) "Hi"))))))
+
+(ert-deftest emacs-tests/seccomp/forbids-subprocess ()
+  (let ((emacs (expand-file-name invocation-name invocation-directory))
+        (filter (ert-resource-file "seccomp-filter.bpf"))
+        (process-environment nil))
+    (skip-unless (file-executable-p emacs))
+    (skip-unless (file-readable-p filter))
+    ;; 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)
+    (with-temp-buffer
+      (let ((status
+             (call-process
+              emacs nil t nil
+              "--quick" "--batch"
+              (concat "--seccomp=" filter)
+              (concat "--eval=" (prin1-to-string
+                                 `(call-process ,emacs nil nil nil
+                                                "--version"))))))
+        (ert-info ((format "Process output: %s" (buffer-string)))
+          (should-not (eql status 0)))))))
+
 ;;; emacs-tests.el ends here
-- 
2.29.2.729.g45daf8777d-goog