summaryrefslogtreecommitdiff
path: root/lisp/emacs-lisp/unsafep.el
blob: d52a6c796dbfc55a9b99c1b5ed36bc9d3f54fe71 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
;;;; unsafep.el -- Determine whether a Lisp form is safe to evaluate  -*- lexical-binding: t; -*-

;; Copyright (C) 2002-2021 Free Software Foundation, Inc.

;; Author: Jonathan Yavner <jyavner@member.fsf.org>
;; Keywords: safety lisp utility

;; 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:

;; This is a simplistic implementation that does not allow any modification of
;; buffers or global variables.  It does no dataflow analysis, so functions
;; like `funcall' and `setcar' are completely disallowed.  It is designed
;; for "pure Lisp" formulas, like those in spreadsheets, that don't make any
;; use of the text editing capabilities of Emacs.

;; A formula is safe if:
;;  1.  It's an atom.
;;  2.  It's a function call to a safe function and all arguments are safe
;;      formulas.
;;  3.  It's a special form whose arguments are like a function's (and,
;;	catch, if, or, prog1, prog2, progn, while, unwind-protect).
;;  4.  It's a special form or macro that creates safe temporary bindings
;;      (condition-case, dolist, dotimes, lambda, let, let*).
;;  4.  It's one of (cond, quote) that have special parsing.
;;  5.  It's one of (add-to-list, setq, push, pop) and the assignment variable
;;      is safe.
;;  6.  It's one of (apply, mapc, mapcar, mapconcat) and its first arg is a
;;      quoted safe function.
;;
;; A function is safe if:
;;  1.  It's a lambda containing safe formulas.
;;  2.  It's a member of list `safe-functions', so the user says it's safe.
;;  3.  It's a symbol with the `side-effect-free' property, defined by the
;;      byte compiler or function author.
;;  4.  It's a symbol with the `safe-function' property, defined here or by
;;      the function author.  Value t indicates a function that is safe but
;;      has innocuous side effects.  Other values will someday indicate
;;      functions with side effects that are not always safe.
;;  The `side-effect-free' and `safe-function' properties are provided for
;;  built-in functions and for functions and macros defined in subr.el.
;;
;; A temporary binding is unsafe if its symbol:
;;  1.  Has the `risky-local-variable' property.
;;  2.  Has a name that ends with -command, font-lock-keywords(-[0-9]+)?,
;;      font-lock-syntactic-keywords, -form, -forms, -frame-alist, -function,
;;       -functions, -history, -hook, -hooks, -map, -map-alist, -mode-alist,
;;       -predicate, or -program.
;;
;; An assignment variable is unsafe if:
;;   1. It would be unsafe as a temporary binding.
;;   2. It doesn't already have a temporary or buffer-local binding.

;; There are unsafe forms that `unsafep' cannot detect.  Beware of these:
;;   1. The form's result is a string with a display property containing a
;;      form to be evaluated later, and you insert this result into a
;;      buffer.  Always remove display properties before inserting!
;;   2. The form alters a risky variable that was recently added to Emacs and
;;      is not yet marked with the `risky-local-variable' property.
;;   3. The form uses undocumented features of built-in functions that have
;;      the `side-effect-free' property.  For example, in Emacs-20 if you
;;      passed a circular list to `assoc', Emacs would crash.  Historically,
;;      problems of this kind have been few and short-lived.

;;; Code:

(provide 'unsafep)
(require 'byte-opt)  ;Set up the `side-effect-free' properties

(defcustom safe-functions nil
  "A list of assumed-safe functions, or t to disable `unsafep'."
  :group 'lisp
  :type  '(choice (const :tag "No" nil) (const :tag "Yes" t) hook))

(defvar unsafep-vars nil
  "Dynamically-bound list of variables with lexical bindings at this point
in the parse.")
(put 'unsafep-vars 'risky-local-variable t)

;; Other safe forms.
;;
;; A function, macro or special form may be put here only if all of
;; the following statements are true:
;;
;; * It is not already marked `pure' or `side-effect-free', or handled
;;   explicitly by `unsafep'.
;;
;; * It is not inherently unsafe; eg, would allow the execution of
;;   arbitrary code, interact with the file system, network or other
;;   processes, or otherwise exfiltrate information from the running
;;   Emacs process or manipulate the user's environment.
;;
;; * It does not have side-effects that can make other code behave in
;;   unsafe and/or unexpected ways; eg, set variables, mutate data, or
;;   change control flow.
;;   Any side effect must be innocuous; altering the match data is
;;   explicitly permitted.
;;
;; * It does not allow Emacs to behave deceptively to the user; eg,
;;   display arbitrary messages.
;;
;; * It does not present a potentially large attack surface; eg,
;;   play arbitrary audio files.

(dolist (x '(;;Special forms
	     and if or prog1 prog2 progn while unwind-protect
	     ;;Safe subrs that have some side-effects
	     ding random sleep-for string-match
	     ;;Defsubst functions from subr.el
	     caar cadr cdar cddr
	     ;;Macros from subr.el
	     save-match-data unless when
	     ;;Functions from subr.el that have side effects
	     split-string))
  (put x 'safe-function t))

;;;###autoload
(defun unsafep (form &optional vars)
  "Return nil if evaluating FORM couldn't possibly do any harm.
Otherwise result is a reason why FORM is unsafe.
VARS is a list of symbols with local bindings like `unsafep-vars'."
  (catch 'unsafep
    (if (or (eq safe-functions t)	    ;User turned off safety-checking
	    (atom form))		    ;Atoms are never unsafe
	(throw 'unsafep nil))
    (let* ((unsafep-vars vars)
	   (fun    (car form))
	   (reason (unsafep-function fun))
	   arg)
      (cond
       ((not reason)
	;;It's a normal function - unsafe if any arg is
	(unsafep-progn (cdr form)))
       ((eq fun 'quote)
	;;Never unsafe
	nil)
       ((memq fun '(apply mapc mapcar mapconcat))
	;;Unsafe if 1st arg isn't a quoted lambda
	(setq arg (cadr form))
	(cond
	 ((memq (car-safe arg) '(quote function))
	  (setq reason (unsafep-function (cadr arg))))
	 ((eq (car-safe arg) 'lambda)
	  ;;Self-quoting lambda
	  (setq reason (unsafep arg unsafep-vars)))
	 (t
	  (setq reason `(unquoted ,arg))))
	(or reason (unsafep-progn (cddr form))))
       ((eq fun 'lambda)
	;;First arg is temporary bindings
	(mapc #'(lambda (x)
		  (or (memq x '(&optional &rest))
		      (let ((y (unsafep-variable x t)))
			(if y (throw 'unsafep y))
			(push x unsafep-vars))))
	      (cadr form))
	(unsafep-progn (cddr form)))
       ((eq fun 'let)
	;;Creates temporary bindings in one step
	(setq unsafep-vars (nconc (mapcar #'unsafep-let (cadr form))
				  unsafep-vars))
	(unsafep-progn (cddr form)))
       ((eq fun 'let*)
	;;Creates temporary bindings iteratively
	(dolist (x (cadr form))
	  (push (unsafep-let x) unsafep-vars))
	(unsafep-progn (cddr form)))
       ((eq fun 'setq)
	;;Safe if odd arguments are local-var syms, evens are safe exprs
	(setq arg (cdr form))
	(while arg
	  (setq reason (or (unsafep-variable (car arg) nil)
			   (unsafep (cadr arg) unsafep-vars)))
	  (if reason (throw 'unsafep reason))
	  (setq arg (cddr arg))))
       ((eq fun 'pop)
	;;safe if arg is local-var sym
	(unsafep-variable (cadr form) nil))
       ((eq fun 'push)
	;;Safe if 2nd arg is a local-var sym
	(or (unsafep (cadr form) unsafep-vars)
	    (unsafep-variable (nth 2 form) nil)))
       ((eq fun 'add-to-list)
	;;Safe if first arg is a quoted local-var sym
	(setq arg (cadr form))
	(if (not (eq (car-safe arg) 'quote))
	    `(unquoted ,arg)
	  (or (unsafep-variable (cadr arg) nil)
	      (unsafep-progn (cddr form)))))
       ((eq fun 'cond)
	;;Special form with unusual syntax - safe if all args are
	(dolist (x (cdr form))
	  (setq reason (unsafep-progn x))
	  (if reason (throw 'unsafep reason))))
       ((memq fun '(dolist dotimes))
	;;Safe if COUNT and RESULT are safe.  VAR is bound while checking BODY.
	(setq arg (cadr form))
	(or (unsafep-progn (cdr arg))
	    (let ((unsafep-vars (cons (car arg) unsafep-vars)))
	      (unsafep-progn (cddr form)))))
       ((eq fun 'condition-case)
	;;Special form with unusual syntax - safe if all args are
	(or (unsafep-variable (cadr form) t)
	    (unsafep (nth 2 form) unsafep-vars)
	    (let ((unsafep-vars (cons (cadr form) unsafep-vars)))
	      ;;var is bound only during handlers
	      (dolist (x (nthcdr 3 form))
		(setq reason (unsafep-progn (cdr x)))
		(if reason (throw 'unsafep reason))))))
       ((eq fun '\`)
	;; Backquoted form - safe if its expansion is.
	(unsafep (cdr (backquote-process (cadr form)))))
       (t
	;;First unsafep-function call above wasn't nil, no special case applies
	reason)))))


(defun unsafep-function (fun)
  "Return nil if FUN is a safe function.
\(Either a safe lambda or a symbol that names a safe function).
Otherwise result is a reason code."
  (cond
   ((eq (car-safe fun) 'lambda)
    (unsafep fun unsafep-vars))
   ((not (and (symbolp fun)
	      (or (get fun 'side-effect-free)
		  (eq (get fun 'safe-function) t)
		  (eq safe-functions t)
		  (memq fun safe-functions))))
    `(function ,fun))))

(defun unsafep-progn (list)
  "Return nil if all forms in LIST are safe.
Else, return the reason for the first unsafe form."
  (catch 'unsafep-progn
    (let (reason)
      (dolist (x list)
	(setq reason (unsafep x unsafep-vars))
	(if reason (throw 'unsafep-progn reason))))))

(defun unsafep-let (clause)
  "Check the safety of a let binding.
CLAUSE is a let-binding, either SYM or (SYM) or (SYM VAL).
Check VAL and throw a reason to `unsafep' if unsafe.
Return SYM."
  (let (reason sym)
    (if (atom clause)
	(setq sym clause)
      (setq sym    (car clause)
	    reason (unsafep (cadr clause) unsafep-vars)))
    (setq reason (or (unsafep-variable sym t) reason))
    (if reason (throw 'unsafep reason))
    sym))

(defun unsafep-variable (sym to-bind)
  "Return nil if SYM is safe to set or bind, or a reason why not.
If TO-BIND is nil, check whether SYM is safe to set.
If TO-BIND is t, check whether SYM is safe to bind."
  (cond
   ((not (symbolp sym))
    `(variable ,sym))
   ((risky-local-variable-p sym nil)
    `(risky-local-variable ,sym))
   ((not (or to-bind
	     (memq sym unsafep-vars)
	     (local-variable-p sym)))
    `(global-variable ,sym))))

;;; unsafep.el ends here