Search code examples
emacshookorg-modelocal-variables

Where does this Emacs Lisp hook function get its argument from?


I'm just learning emacs and came across a configuration that demonstrates quite a bit of the functionality I want in my own configuration. It uses init.el as an entry point to an org file that handles the bulk of the configuration with extensive documentation. I am confused by the following function:

(put 'after-save-hook 'safe-local-variable
       (lambda (value) (equal value '(org-babel-tangle t))))

What I think I understand is that this puts the value of the lambda expression into the property list of after-save-hook under the property name safe-local-variable, and that a value is safe if, when passed to the safe-local-variable-p function it returns a non-nil value. This lambda then appears to do an equality comparison between value and the list (org-babel-tangle t), so presumably this means that value is safe only when it's equal to the list (org-babel-tangle t)?

What I am having trouble understanding is twofold. First, where is the lambda getting value from? Second, what is this all actually doing? Neither the documentation I could find on after-save-hook nor org-babel-tangle clarified this for me. The author's comments say "Mark safe variables early so that tangling won't break," but I still don't get it.


Solution

  • The reason for this is that the file DESKTOP.org starts with this line:

    # -*- after-save-hook: (org-babel-tangle t); -*-
    

    That is, when you open that file in Emacs, the local value of after-save-hook becomes (org-babel-tangle t). So whenever you save that file, it's going to call the function org-babel-tangle without any arguments, in order to generate a few shell scripts, e.g. scripts/screenshot.region.sh. The value t in a hook variable means that after calling all the functions in the local value of the hook variable, run-hooks is going to look at the global value of after-save-hook and call any functions listed there as well.

    Obviously, allowing any file to specify arbitrary Lisp code to be run would be a security hole equaled only by Microsoft Word macros, so by default file-local settings for risky variables are ignored. Since we know this particular value is safe,* we use the safe-local-variable trick you're asking about. As per the documentation:

    You can specify safe values for a variable with a ‘safe-local-variable’ property. The property has to be a function of one argument; any value is safe if the function returns non-‘nil’ given that value.

    So that's what we have here:

    (lambda (value) (equal value '(org-babel-tangle t)))
    

    It is a function that takes one argument, and checks that the argument is equal to the specific value we want to allow. safe-local-variable-p is going to call this function on the specified file-local value, and only allow it if the function returns non-nil. Thus, value is going to be the value that is about to be assigned to after-save-hook. We can see that in action in M-x ielm:

    *** Welcome to IELM ***  Type (describe-mode) for help.
    ELISP> (setq my-function (lambda (value) (equal value '(org-babel-tangle t))))
    (lambda
      (value)
      (equal value
             '(org-babel-tangle t)))
    
    ELISP> (funcall my-function '(org-babel-tangle t))
    t
    ELISP> (funcall my-function 'something-else)
    nil
    

    * Is this safe, though?... If an attacker can get you to download a specially crafted file and run org-babel-tangle on it, they can overwrite arbitrary files in the file system using the privileges of your user. It's not what's happening in this case, just something to be aware of.