daroczig
daroczig

Reputation: 28672

Scoping (functions) in custom environment

I have a special (dummy) function which I want to use in a sandboxed environment:

disable.system.call <- function(...) {
    mc <- match.call()
    if (grepl('system', deparse(mc[[2]])))
        stop('NONO')
    eval(mc, env = .GlobalEnv)        
}

It does nothing special just checks if the first argument has the system word in its name. This is just a POC example.

What I do later: I assign this simple functions to some base and stats functions to see if evaluated expressions do contain the system word as the first argument. E.g.:

e <- new.env()
eval(parse(text = 'model.frame <- disable.system.call'), envir = e)

This works pretty cool, as calls without system inside works like a charm, but the filter works:

> eval(parse(text = 'model.frame("1 ~ 1")'), envir = e)
  1
1 1
> eval(parse(text = 'model.frame(\'1 ~ system("ls -la")\')'), envir = e)
Error in model.frame("1 ~ system(\"ls -la\")") : NONO

It's even working with an lm call which calls model.frame inside of found a formula like string:

> eval(parse(text = 'lm(\'1 ~ system("ls -la")\')'), envir = e)
Error in model.frame(formula = "1 ~ system(\"ls -la\")", drop.unused.levels = TRUE) : 
  NONO

I tried to go a bit further and assigned that pretty simple function (disable.system.call) to as.formula which is called from model.frame. Unfortunately I did not got so far:

> e <- new.env()
> eval(parse(text = 'as.formula <- disable.system.call'), envir = e)
> eval(parse(text = 'as.formula("1 ~ 1")'), envir = e)
1 ~ 1
> eval(parse(text = 'as.formula(\'1 ~ system("ls -la")\')'), envir = e)
Error in as.formula("1 ~ system(\"ls -la\")") : NONO
> eval(parse(text = 'model.frame(\'1 ~ system("ls -la")\')'), envir = e)
  1 system("ls -la")
1 1                0
> eval(parse(text = 'lm(\'1 ~ system("ls -la")\')'), envir = e)

Call:
lm(formula = "1 ~ system(\"ls -la\")")

Coefficients:
     (Intercept)  system("ls -la")  
           1                NA  

As I know model.frame is calling as.formula but this does not work (as you can see from the above output). I am quite sure it's not because model.frame is calling stats::as.formula as lm called above model.frame in the custom environment.

Any hints and ideas would be really welcomed!

Upvotes: 4

Views: 283

Answers (2)

Richie Cotton
Richie Cotton

Reputation: 121167

If you don't want people to be able to use system, it would be easier to overwrite the definition.

assignInNamespace(
  "system", 
  function(...) stop("system calls are not allowed"), 
  getNamespace("base")
)

system("pwd")  #throws an error

I'm wildly guessing at your use case, but are you letting users pass arbitrary R code to some other application? In which case you probably want to compile your own version of R, with the dangerous functions removed or replaced by dummies.


One other possibility for executing custom code when a function is called is trace. For example,

trace(system, quote(stop("You have called system")))  #you may also want print = FALSE

Upvotes: 3

Josh O&#39;Brien
Josh O&#39;Brien

Reputation: 162461

Although you suspected it wasn't the case, stats:::model.frame.default is being called, instead of the custom version in environment e. (This is of course the behavior you'd generally expect from packaged functions. The odd scoping seen in your first example is a special case, due to lm()'s use of 'non-standard evaluation', which is discussed at the bottom of my answer).

As I show below, you can use trace() to see which version of as.formula() is getting called in each of your cases:

disable.system.call <- function(...) {
    mc <- match.call()
    if (grepl('system', deparse(mc[[2]])))
        stop('NONO')
    eval(mc, env = .GlobalEnv)        
}
e <- new.env()
eval(parse(text = 'as.formula <- disable.system.call'), envir = e)


# (1) trace custom 'as.formula()' in environment e
trace(e$as.formula)


# Calling model.frame() **does not** call the the custom as.formula()
eval(parse(text = 'model.frame(\'1 ~ system("ls -la")\')'), envir = e)
#   1 system("ls -la")
# 1 1              127

# (2) trace stats:::as.formula()
trace(stats:::as.formula)

# Calling model.frame() **does** call stats:::as.formula()
eval(parse(text = 'model.frame(\'1 ~ system("ls -la")\')'), envir = e)
# trace: as.formula
#   1 system("ls -la")
# 1 1              127

Edit: FWIW, the reason that your custom model.frame() was called by lm() in the first example is that lm() employs what is sometimes called 'non-standard evaluation'. (See this pdf for more on the subject than you're likely to ever want.) The key bit is that lm() actually directs model.frame() to be evaluated in the calling environment; in your case, this led to it's finding your version of the function.

The reason that lm() uses non-standard evaluation is so that model.frame() can access variables named in the formula even if they are found in the calling environment (rather than just being able to access variables passed in via the data argument to lm()). As Thomas Lumley says in the linked pdf:

If variables in the formula were required to be in the data argument life would be a lot simpler, but this requirement was not made when formulas were introduced.

In case you're interested, here are the relevant lines from the definition of lm:

mf <- match.call(expand.dots = FALSE)
...
mf[[1L]] <- as.name("model.frame")
mf <- eval(mf, parent.frame())

Upvotes: 4

Related Questions