Subject: 28.0.50; Sandbox mode

From: Stefan Monnier <[email-masked]>
Date: Sat, 12 Dec 2020 13:01:04 -0500

Package: Emacs
Version: 28.0.50

Currently we cannot enable `flymake-mode` by default in `elisp-mode`
for simple reasons of security: having this mode enabled means that
whenever we happen to open a file containing ELisp code, our
Emacs will happily try to compile this file, including evaluating
arbitrary code contained in its macros.

Similarly, elpa.gnu.org can't automatically take a documentation file in
Org format and convert it to Texinfo (and then Info) when generating
a package's tarball, because the Org -> Texinfo conversion can run
arbitrary code.

To try and address these problems I suggest the function `sandbox-enter`
in the patch below, which should let us run untrusted ELisp code safely
(in an Emacs subprocess).

The patch for `elisp-flymake-byte-compile` is just there to give an idea
of how I expect it to work: currently the compiler will still try to write
the result of its compilation, which will fail because we're now
sandboxed, so it needs more work before it behaves like it should.

One thing I'm particularly eager to hear your opinion about is whether
there might be more holes to plug (i.e. more places where we need to
call `ensure_no_sandbox`).  Clearly, from a security perspective, this is
the main drawback of this approach: it's based on a black list rather
than on a whitelist.  Still, I have the impression that it should
be manageable.

        Stefan

diff --git a/etc/NEWS b/etc/NEWS
index 514209516d..9a569b4bd2 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -85,6 +85,16 @@ useful on systems such as FreeBSD which ships only with "etc/termcap".
 
 * Changes in Emacs 28.1
 
+** Sandbox mode to run untrusted code.
+The new function 'sandbox-enter' puts Emacs mode into a state in which
+it can (hopefully) run untrusted code safely.  This mode is restricted such
+that Emacs cannot exit the mode, nor can it write to files or launch
+new processes.  It can still read arbitrary files and communicate over
+pre-existing communication links.  This can only be used in batch mode.
+The expected use is to launch a new batch Emacs session, put it
+into sandbox mode, then run the untrusted code, send back the
+result via 'message', and then exit.
+
 ** Minibuffer scrolling is now conservative by default.
 This is controlled by the new variable 'scroll-minibuffer-conservatively'.
 
diff --git a/lisp/progmodes/elisp-mode.el b/lisp/progmodes/elisp-mode.el
index fa360a8f3f..189ee896b6 100644
--- a/lisp/progmodes/elisp-mode.el
+++ b/lisp/progmodes/elisp-mode.el
@@ -1839,6 +1839,7 @@ elisp-flymake--batch-compile-for-flymake
             (push (list string position fill level)
                   collected)
             t)))
+    (sandbox-enter)     ;FIXME: this will break the subsequent delete-file!
     (unwind-protect
         (byte-compile-file file)
       (ignore-errors
diff --git a/src/emacs-module.c b/src/emacs-module.c
index 0f3ef59fd8..1bebdfeb4a 100644
--- a/src/emacs-module.c
+++ b/src/emacs-module.c
@@ -1091,6 +1091,7 @@ DEFUN ("module-load", Fmodule_load, Smodule_load, 1, 1, 0,
        doc: /* Load module FILE.  */)
   (Lisp_Object file)
 {
+  ensure_no_sandbox ();
   dynlib_handle_ptr handle;
   emacs_init_function module_init;
   void *gpl_sym;
@@ -1151,6 +1152,7 @@ DEFUN ("module-load", Fmodule_load, Smodule_load, 1, 1, 0,
 Lisp_Object
 funcall_module (Lisp_Object function, ptrdiff_t nargs, Lisp_Object *arglist)
 {
+  ensure_no_sandbox ();
   const struct Lisp_Module_Function *func = XMODULE_FUNCTION (function);
   eassume (0 <= func->min_arity);
   if (! (func->min_arity <= nargs
diff --git a/src/emacs.c b/src/emacs.c
index 2a32083ba1..0df70ae43e 100644
--- a/src/emacs.c
+++ b/src/emacs.c
@@ -139,6 +139,8 @@ #define MAIN_PROGRAM
 struct gflags gflags;
 bool initialized;
 
+bool emacs_is_sandboxed;
+
 /* If true, Emacs should not attempt to use a window-specific code,
    but instead should use the virtual terminal under which it was started.  */
 bool inhibit_window_system;
@@ -2886,6 +2888,30 @@ DEFUN ("daemon-initialized", Fdaemon_initialized, Sdaemon_initialized, 0, 0, 0,
   return Qt;
 }
 
+DEFUN ("sandbox-enter", Fsandbox_enter, Ssandbox_enter, 0, 0, 0,
+  doc: /* Put Emacs in sandboxed mode.
+After calling this function, Emacs cannot have externally visible
+side effects any more.  For example, it will not be able to write to files,
+create new processes, open new displays, or call functionality provided by
+modules, ...
+It can still send data to pre-existing processes, on the other hand.
+There is no mechanism to exit sandboxed mode.  */)
+  (void)
+{
+  if (!noninteractive)
+    /* We could allow a sandbox in interactive sessions, but it seems
+       useless, so we'll assume that it was a pilot-error.  */
+    error ("Can't enter sandbox in interactive session");
+  emacs_is_sandboxed = true;
+  return Qnil;
+}
+
+void ensure_no_sandbox (void)
+{
+  if (emacs_is_sandboxed)
+    error ("Sandboxed");
+}
+
 void
 syms_of_emacs (void)
 {
@@ -2906,6 +2932,7 @@ syms_of_emacs (void)
   defsubr (&Sinvocation_directory);
   defsubr (&Sdaemonp);
   defsubr (&Sdaemon_initialized);
+  defsubr (&Ssandbox_enter);
 
   DEFVAR_LISP ("command-line-args", Vcommand_line_args,
 	       doc: /* Args passed by shell to Emacs, as a list of strings.
diff --git a/src/fileio.c b/src/fileio.c
index 702c143828..e221048493 100644
--- a/src/fileio.c
+++ b/src/fileio.c
@@ -689,6 +689,7 @@ DEFUN ("make-temp-file-internal", Fmake_temp_file_internal,
   (Lisp_Object prefix, Lisp_Object dir_flag, Lisp_Object suffix,
    Lisp_Object text)
 {
+  ensure_no_sandbox ();
   CHECK_STRING (prefix);
   CHECK_STRING (suffix);
   Lisp_Object encoded_prefix = ENCODE_FILE (prefix);
@@ -2043,6 +2044,7 @@ DEFUN ("copy-file", Fcopy_file, Scopy_file, 2, 6,
    Lisp_Object keep_time, Lisp_Object preserve_uid_gid,
    Lisp_Object preserve_permissions)
 {
+  ensure_no_sandbox ();
   Lisp_Object handler;
   ptrdiff_t count = SPECPDL_INDEX ();
   Lisp_Object encoded_file, encoded_newname;
@@ -2301,6 +2303,7 @@ DEFUN ("make-directory-internal", Fmake_directory_internal,
        doc: /* Create a new directory named DIRECTORY.  */)
   (Lisp_Object directory)
 {
+  ensure_no_sandbox ();
   const char *dir;
   Lisp_Object handler;
   Lisp_Object encoded_dir;
@@ -2327,6 +2330,7 @@ DEFUN ("delete-directory-internal", Fdelete_directory_internal,
        doc: /* Delete the directory named DIRECTORY.  Does not follow symlinks.  */)
   (Lisp_Object directory)
 {
+  ensure_no_sandbox ();
   const char *dir;
   Lisp_Object encoded_dir;
 
@@ -2356,6 +2360,7 @@ DEFUN ("delete-file", Fdelete_file, Sdelete_file, 1, 2,
 With a prefix argument, TRASH is nil.  */)
   (Lisp_Object filename, Lisp_Object trash)
 {
+  ensure_no_sandbox ();
   Lisp_Object handler;
   Lisp_Object encoded_file;
 
@@ -2494,6 +2499,7 @@ DEFUN ("rename-file", Frename_file, Srename_file, 2, 3,
 This is what happens in interactive use with M-x.  */)
   (Lisp_Object file, Lisp_Object newname, Lisp_Object ok_if_already_exists)
 {
+  ensure_no_sandbox ();
   Lisp_Object handler;
   Lisp_Object encoded_file, encoded_newname;
 
@@ -2614,6 +2620,7 @@ DEFUN ("add-name-to-file", Fadd_name_to_file, Sadd_name_to_file, 2, 3,
 This is what happens in interactive use with M-x.  */)
   (Lisp_Object file, Lisp_Object newname, Lisp_Object ok_if_already_exists)
 {
+  ensure_no_sandbox ();
   Lisp_Object handler;
   Lisp_Object encoded_file, encoded_newname;
 
@@ -2667,6 +2674,7 @@ DEFUN ("make-symbolic-link", Fmake_symbolic_link, Smake_symbolic_link, 2, 3,
 This happens for interactive use with M-x.  */)
   (Lisp_Object target, Lisp_Object linkname, Lisp_Object ok_if_already_exists)
 {
+  ensure_no_sandbox ();
   Lisp_Object handler;
   Lisp_Object encoded_target, encoded_linkname;
 
@@ -3176,6 +3184,7 @@ DEFUN ("set-file-selinux-context", Fset_file_selinux_context,
 or if Emacs was not compiled with SELinux support.  */)
   (Lisp_Object filename, Lisp_Object context)
 {
+  ensure_no_sandbox ();
   Lisp_Object absname;
   Lisp_Object handler;
 #if HAVE_LIBSELINUX
@@ -3307,6 +3316,7 @@ DEFUN ("set-file-acl", Fset_file_acl, Sset_file_acl,
 support.  */)
   (Lisp_Object filename, Lisp_Object acl_string)
 {
+  ensure_no_sandbox ();
 #if USE_ACL
   Lisp_Object absname;
   Lisp_Object handler;
@@ -3392,6 +3402,7 @@ DEFUN ("set-file-modes", Fset_file_modes, Sset_file_modes, 2, 3,
 symbolic notation, like the `chmod' command from GNU Coreutils.  */)
   (Lisp_Object filename, Lisp_Object mode, Lisp_Object flag)
 {
+  ensure_no_sandbox ();
   CHECK_FIXNUM (mode);
   int nofollow = symlink_nofollow_flag (flag);
   Lisp_Object absname = Fexpand_file_name (filename,
@@ -3458,6 +3469,7 @@ DEFUN ("set-file-times", Fset_file_times, Sset_file_times, 1, 3, 0,
 TIMESTAMP is in the format of `current-time'. */)
   (Lisp_Object filename, Lisp_Object timestamp, Lisp_Object flag)
 {
+  ensure_no_sandbox ();
   int nofollow = symlink_nofollow_flag (flag);
 
   struct timespec ts[2];
@@ -5039,6 +5051,7 @@ DEFUN ("write-region", Fwrite_region, Swrite_region, 3, 7,
   (Lisp_Object start, Lisp_Object end, Lisp_Object filename, Lisp_Object append,
    Lisp_Object visit, Lisp_Object lockname, Lisp_Object mustbenew)
 {
+  ensure_no_sandbox ();
   return write_region (start, end, filename, append, visit, lockname, mustbenew,
 		       -1);
 }
@@ -5837,6 +5850,7 @@ DEFUN ("do-auto-save", Fdo_auto_save, Sdo_auto_save, 0, 2, "",
 A non-nil CURRENT-ONLY argument means save only current buffer.  */)
   (Lisp_Object no_message, Lisp_Object current_only)
 {
+  ensure_no_sandbox ();
   struct buffer *old = current_buffer, *b;
   Lisp_Object tail, buf, hook;
   bool auto_saved = 0;
diff --git a/src/lisp.h b/src/lisp.h
index e83304462f..107a2d9f03 100644
--- a/src/lisp.h
+++ b/src/lisp.h
@@ -603,6 +603,10 @@ #define ENUM_BF(TYPE) enum TYPE
    subsequent starts.  */
 extern bool initialized;
 
+/* Whether we've been neutralized.  */
+extern bool emacs_is_sandboxed;
+extern void ensure_no_sandbox (void);
+
 extern struct gflags
 {
   /* True means this Emacs instance was born to dump.  */
diff --git a/src/process.c b/src/process.c
index 4fe8ac7fc0..68460868c4 100644
--- a/src/process.c
+++ b/src/process.c
@@ -862,6 +862,8 @@ allocate_pty (char pty_name[PTY_NAME_SIZE])
 static struct Lisp_Process *
 allocate_process (void)
 {
+  ensure_no_sandbox ();
+
   return ALLOCATE_ZEROED_PSEUDOVECTOR (struct Lisp_Process, thread,
 				       PVEC_PROCESS);
 }
diff --git a/src/xdisp.c b/src/xdisp.c
index 96dd4fade2..72c37e8194 100644
--- a/src/xdisp.c
+++ b/src/xdisp.c
@@ -15442,6 +15442,10 @@ #define RESUME_POLLING					\
 static void
 redisplay_internal (void)
 {
+  /* Not sure if it's important to avoid redisplay inside a sandbox,
+     but it seems safer to avoid risking introducing security holes via
+     image libraries and such.  */
+  ensure_no_sandbox ();
   struct window *w = XWINDOW (selected_window);
   struct window *sw;
   struct frame *fr;
diff --git a/src/xterm.c b/src/xterm.c
index 3de0d2e73c..e8a4d2f29d 100644
--- a/src/xterm.c
+++ b/src/xterm.c
@@ -12698,6 +12698,7 @@ x_term_init (Lisp_Object display_name, char *xrm_option, char *resource_name)
   xcb_connection_t *xcb_conn;
 #endif
 
+  ensure_no_sandbox ();
   block_input ();
 
   if (!x_initialized)