kanru / cl-isolated

A restricted environment for Common Lisp code evaluation
GNU Affero General Public License v3.0
39 stars 3 forks source link

eval-return-values branch discussion #4

Open Harag opened 4 years ago

Harag commented 4 years ago

To make cl-isolated to work better as a scripting engine we need the following changes. I have implemented those changes in the eval-return-values branch.

Please comment.

I might have gone a bit over board with code etc in the discussion but since its the first majour change to cl-isolated in many moons I would like the communication to be clear.

  1. Easily "allow" an additional set of functionality to be add to an instance of cl-isolate before trying to run/eval code with cl-isolate. Adding additional symbols is not enough we need to be able to add functions and/or even whole packages.

For that to work I need to make a compatibility breaking change.

(defvar *allowed-extra-symbols* nil) needs to be replaced by

(defvar *allowed-isolated-symbols* nil)
(defvar *allowed-isolated-functions* nil)

Functions etc to add functionality to cl-isolated:

(defvar *allowed-packages-symbols* nil)
(defvar *allowed-packages-functions* nil)

(defun set-allowed-symbol (symbol)
  (if (fboundp symbol)
      (push symbol *allowed-packages-functions*)
      (push symbol *allowed-packages-symbols*)))

(defun get-package-symbols (packages &optional excluded-symbols)
  (let (symbols)
    (dolist (package packages)
      (do-external-symbols (s (find-package package))
    (unless (find s excluded-symbols :test 'equalp)
      (push s symbols))))
    symbols))

(defun allow-symbols (symbols)
  (dolist (symbol symbols)
      (set-allowed-symbol symbol)))

(defun allow-package-symbols (packages &optional excluded-symbols)
  (unless *allowed-packages-symbols*
    (dolist (package packages)
    (do-external-symbols (symbol (find-package package))
      (unless (find symbol excluded-symbols :test 'equalp)
        (set-allowed-symbol symbol))))))
  1. We need translate-form to do a touch more checking/validation ie checks before eval

a. Throw an error if something is not allowed by cl-isolate

To not have to loop each time the check needs to be done we need the following convenience function and vars

(defvar *allowed-isolated-symbols* nil)
(defvar *allowed-isolated-functions* nil)

(defun isolated-allowed-symbols ()
  (loop :for symbol :being :the :symbol :in (find-package 'isolated-cl)
     :when (not (get symbol :isolated-locked))
     :do
       (if (fboundp symbol)
       (push symbol *allowed-isolated-functions*)
       (push symbol *allowed-isolated-symbols*))))

b. Differentiate between symbols and functions when translating code and throwing errors.

(defun translate-form (form)
  (when (and (consp form)
             (circular-tree-p form))
    (error 'circular-list))
  (let ((cons-count 0))
    (labels ((translate (form)
               (typecase form
                 (cons (if (> (incf cons-count) *max-elements*)
                           (error 'dimension-error)
                           (cons (translate (car form))
                                 (translate (cdr form)))))
                 (number form)
                 (character form)
                 (pathname form)
                 (array (if (> (array-total-size form) *max-elements*)
                            (error 'dimension-error)
                            (let ((arr (make-array (array-dimensions form)
                                                   :element-type
                                                   (array-element-type form))))
                              (dotimes (i (array-total-size arr) arr)
                                (setf (row-major-aref arr i)
                                      (translate-validate-form
                                       (row-major-aref form i)))))))
                 (keyword form)
                 (symbol (if (fboundp form)
                 (or (find form *allowed-isolated-functions*)
                 (find form *allowed-packages-functions*)
                 (error 'undefined-function :name form))
                 (if (or (find form *allowed-isolated-symbols*)
                     (find form *allowed-packages-symbols*))
                 form
                 (intern (symbol-name form) *env*))))
                 (t (error 'unsupported-type :type (type-of form))))))
      (translate form))))
  1. Allow code to be passed to cl-isolate that is not in a string but in sexp already. Working with strings is just no fun because you have to deal with " etc when creating the code to be feed to cl-isolated.

+

  1. Return/Expose the results of one or more of the sexps in the code fed to cl-isolated.
    
    (isolated-allowed-symbols)

(defun reset () (setf isolated-impl::allowed-isolated-symbols nil) (setf isolated-impl::allowed-isolated-functions nil) (setf isolated-impl::allowed-packages-symbols nil) (setf isolated-impl::allowed-packages-functions nil)

(isolated-allowed-symbols)

(ignore-errors (delete-package env)) (make-package env :use '(#:isolated-cl)) (loop :for name :in '("+" "++" "+++" "" "" "" "/" "//" "///" "-") :do (eval (defparameter ,(intern name *env*) nil))) (loop :for fn :in '(+ - * /) :for symbol := (intern (symbol-name fn) *env*) :do (setf (get symbol :isolated-locked) t) (eval(defun ,symbol (&rest args) (apply ',fn args)))) env)

(defun read-no-eval (forms &key packages exclude-symbols) "Returns forms and/or any messages." (unless (or (find-package env) (reset))
(return-from read-no-eval "ISOLATED-PACKAGE-ERROR: Isolated package not found."))

(allow-package-symbols packages exclude-symbols)

(let ((validated-forms) (msg))

(labels ((sexp-read (sexps)
       (let (values)
     (if (listp (car sexps))            
         (dolist (sexp sexps)         
           (push (translate-form sexp) values))
         (push (translate-form sexps) values))
     (reverse values)))

     (sread (string)
       (let (values)
     (with-input-from-string (s string)         
       (loop for sexp = (read s nil)
          while sexp
          do
        (if (listp (car sexp))
            (dolist (sexpx sexp)
              (push (translate-form sexpx)
                values))
            (push (translate-form sexp)
              values))))
     (reverse values))))

  (setf validated-forms
    (if (stringp forms)
    (sread forms)
    (sexp-read forms))))
(values validated-forms msg)))

(defun read-eval (forms &key packages exclude-symbols) "Returns eval values and/or any messages."

(unless (or (find-package env) (reset)) (return-from read-eval (values nil "ISOLATED-PACKAGE-ERROR: Isolated package not found.")))

(allow-package-symbols packages exclude-symbols)

(with-isolated-env (let ((values) (msg))

  (flet ((sexp-read (sexps)
       (let (values)
     (if (listp (car sexps))
         (dolist (sexp sexps)
           (push (multiple-value-list
              (eval
               (translate-form sexp)))
             values))
         (push (multiple-value-list
            (eval
             (translate-form sexps)))
           values))  
     (reverse values)))
     (sread (string)
       (let (values)
     (with-input-from-string (s string)         
       (loop for sexp = (read s nil)
          while sexp
          do
        (multiple-value-list
         (if (listp (car sexp))
             (dolist (sexpx sexp)
               (push (multiple-value-list
                  (eval
                   (translate-form sexpx)))
                 values))
             (push (multiple-value-list
                (eval
                 (translate-form sexp)))
               values)))))
     (reverse values))))
(setf values (if (stringp forms)
         (sread forms)
         (sexp-read forms))))
  (values values msg))))

(defun ssetq (name value) (setf (symbol-value (find-symbol (string-upcase name) env)) value))

(defun read-eval-print (forms &optional (stream standard-output)) (unless (or (find-package env) (reset)) (msge stream "ISOLATED-PACKAGE-ERROR: Isolated package not found.") (return-from read-eval-print nil))

(with-isolated-env (let (form)

  (flet ((sexp-read (sexps)
       (let (values)
     (if (listp (car sexps))
         (dolist (sexp sexps)
           (push (multiple-value-list
              (eval
               (translate-form sexp)))
             values))
         (push (multiple-value-list
            (eval
             (translate-form sexps)))
           values))  
     (reverse values)))

     (sread (string)
       (let (values)
     (with-input-from-string (s string)         
       (loop for sexp = (read s nil)
          while sexp
          do
        (multiple-value-list
         (if (listp (car sexp))                  
             (dolist (sexpx sexp)
               (setf form (translate-form sexpx))
               (push (multiple-value-list
                  (eval
                   (prog1
                   form
                 (ssetq "-" form))))
                 values))
             (progn
               (setf form (translate-form sexp))
               (push (multiple-value-list
                  (eval
                   (prog1
                   form
                 (ssetq "-" form))
                   ))
                 values))))))
     (reverse values)))

     (muffle (c)
       (declare (ignore c))
       (when (find-restart 'muffle-warning)
     (muffle-warning))))

(let (form values)

  (handler-case
      (handler-bind ((warning #'muffle))

    (setf values (if (stringp forms)
             (sread forms)
             (sexp-read forms)))
    (dolist (value values)
      (isolated-print value stream)))

    (undefined-function (c)
      (msge stream "~A: The function ~A is undefined."
        (type-of c) (cell-error-name c)))

    (end-of-file (c)
      (msge stream "~A" (type-of c)))

    (reader-error ()
      (msge stream "READER-ERROR"))

    (package-error ()
      (msge stream "PACKAGE-ERROR"))

    (stream-error (c)
      (msge stream "~A" (type-of c)))

    (storage-condition ()
      (msge stream "STORAGE-CONDITION"))

    (t (c)
      (msge stream "~A: ~A" (type-of c) c)))

  (flet ((svalue (string)
       (symbol-value (find-symbol string *env*))))
    (ssetq "///" (svalue "//"))
    (ssetq "//"  (svalue "/"))
    (ssetq "/"   values)
    (ssetq "***" (svalue "**"))
    (ssetq "**"  (svalue "*"))
    (ssetq "*"   (first values))
    (ssetq "+++" (svalue "++"))
    (ssetq "++"  (svalue "+"))
    (ssetq "+"   form))))))

nil)

Examples:
```lisp
(isolated::read-no-eval (list '(princ-to-string '(hello world))
                                            '(princ-to-string '(eish world))))

((PRINC-TO-STRING '(ISOLATED/LOCAL::HELLO ISOLATED/LOCAL::WORLD))
 (PRINC-TO-STRING '(ISOLATED/LOCAL::EISH ISOLATED/LOCAL::WORLD)))
NIL

(isolated::read-eval (list '(princ-to-string '(hello world))
                                      '(princ-to-string '(eish world))))
(("(HELLO WORLD)") ("(EISH WORLD)"))
NIL

(isolated::read-eval-print (list '(princ-to-string '(hello world))
                                               '(princ-to-string '(eish world))))
=> "(HELLO WORLD)"
=> "(EISH WORLD)"
NIL

(isolated:read-eval-print "(princ-to-string '(hello world)) (princ-to-string '(eish world))")
=> "(HELLO WORLD)"
=> "(EISH WORLD)"
NIL

Examples Allowing additional functions:

CL-USER> (defun do-eish (eish) eish)
DO-EISH

CL-USER> (isolated:read-eval-print "(do-eish 'eish)")
;; UNDEFINED-FUNCTION: The function DO-EISH is undefined.

CL-USER>  (isolated-impl:allow-symbols (list 'do-eish))

CL-USER> (isolated::read-no-eval "(cl-user::do-eish 'cl-user::eish)")
((DO-EISH 'ISOLATED/LOCAL::EISH))
NIL

CL-USER>  (isolated-impl:allow-symbols (list 'do-eish 'eish))

CL-USER> (isolated::read-no-eval "(cl-user::do-eish 'cl-user::eish)")
((DO-EISH 'EISH))
NIL

(isolated::read-eval-print "(cl-user::do-eish 'eish)")
=> EISH
NIL
CL-USER> (isolated::read-eval-print "(cl-user::do-eish 'cl-user::eish)")
=> COMMON-LISP-USER::EISH
NIL
Harag commented 4 years ago

I had to tweak translate-form to deal with defun and defmacro. Because we are now keeping more detailed track of which functions are allowed and which are not I have to update the tracking while parsing defun(s). There might be a better way to do it, for instance to move tracking down to isolated-cl and making the macros deal with it but for now the current hack should work for most scenarios.

;; To keep track of functions created by user in the scripts/code
(defparameter *allowed-internal-functions* nil)

;; keeping track of previous form in translate so that I can identify which functions are added in the 
;; form submitted
(defparameter *previous-form* nil)

(defun translate-form (form)
  (when (and (consp form)
             (circular-tree-p form))
    (error 'circular-list))
  (let ((cons-count 0))
    (labels ((translate (form)
               (typecase form
                 (cons (if (> (incf cons-count) *max-elements*)
                           (error 'dimension-error)
                           (cons (translate (car form))
                                 (translate (cdr form)))))
                 (number form)
                 (character form)
                 (pathname form)
                 (array (if (> (array-total-size form) *max-elements*)
                            (error 'dimension-error)
                            (let ((arr (make-array (array-dimensions form)
                                                   :element-type
                                                   (array-element-type form))))
                              (dotimes (i (array-total-size arr) arr)
                                (setf (row-major-aref arr i)
                                      (translate-form
                                       (row-major-aref form i)))))))
                 (keyword form)
                 (symbol

          (when (or (equalp *previous-form* 'isolated-cl::defun)
                (equalp *previous-form* 'isolated-cl::defmacro)
                (equalp *previous-form* 'cl::defun)
                (equalp *previous-form* 'cl::defmacro))
            (pushnew form *allowed-internal-functions*))

          (let ((final-form
             (if (fboundp form)
                 (or (find form *allowed-isolated-functions*)
                 (find form *allowed-packages-functions*)
                 (find form *allowed-internal-functions*)
                 (or
                  (and (equalp form 'isolated-cl::defun) form)
                  (and (equalp form 'isolated-cl::defmacro) form)
                  (and (equalp form 'cl:defun) form)
                  (and (equalp form 'cl:defmacro) form))

                 (error 'undefined-function :name form))
                 (if (or (find form *allowed-isolated-symbols*)
                     (find form *allowed-packages-symbols*))
                 form
                 (intern (symbol-name form) *env*)))))
            (setf *previous-form* final-form)

            final-form))
                 (t (error 'unsupported-type :type (type-of form))))))
      (translate form))))