Reputation: 28672
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
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
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