This is a read-only snapshot of the ComputerCraft forums, taken in April 2020.
Piorjade's profile picture

About loading files in a program and giving them a "sandbox" enviroment

Started by Piorjade, 19 September 2016 - 11:12 AM
Piorjade #1
Posted 19 September 2016 - 01:12 PM
While I was updating cLinux, I thought about how I could load a file from /usr/bin, which has the limited FS API.
It works nicely and returns errors if the program is crashing BUT on the other hand you can easily edit the _G.currentUsr (I tried setting currentUsr to local and NOT copying it over to _G.currentUsr, but the program can still edit it. I guess it has to do with the fact, that I load the programs with loadfile() ) and _G.currentPw to grant you root access.

So I thought about "copying" the whole Enviroment os.lua is in (I copied _G, if that's right) on nother enviroment:

limitFunctions() --This limits the fs API, as I said everything worked, but the user could edit the _G variables to run program as root, without knowing the password (copy the hashed password from /sys/.rootpw)
SandBox = _G --Here I thought the whole enviroment, which my OS was in with the limited FS API, would get copied over
restoreFunctions() --This restores the original fs API, giving the OS' Enviroment normal access

And then did this:


local a = loadfile("/usr/bin/"..command, "prog", "t", SandBox) --I don't know if "prog" does ANYTHING, so I kinda want to know exactly what arguments I have to give to loadfile()
local ok, err = pcall(a, unpack(args))

if ok == false then
  local col = term.getTextColor()
  term.setTextColor(colors.red)
  print(err)
  term.setTextColor(col)
end

And what happens?
Well basically it looks like the OS gets restarted when entering a, in /usr/bin existing, command (or like it gets started AGAIN in another enviroment) and then doesn't restart again but completely shuts down upon the next command (I mean in the terminal when entering a command, which is located in /usr/bin)

Pls halp? :D/>

My goal is to have a command/program (located in /usr/bin) started (with all the variables of _G, e.g. _G.currentUsr) WITHOUT the ability to edit these variables, but I guess the idea above would lead to the executed program having as long rootaccess, as long the program runs, meaning that you could edit the variables at the first line of your program to get full access.

Right now you can run ONE program to edit these variables and this way grant every next program rootaccess.
Edited on 19 September 2016 - 11:13 AM
Piorjade #2
Posted 19 September 2016 - 01:20 PM
I'm going to try something out with writing "currentPw" to a file somewhere a normal user is not allowed to edit..
Meaning the limited FS API wouldn't check from a variable which password was entered, but open that file, read it, check if it's the same as the root password, and then return..
Maybe I got a fix, but I still want to know from you guys, if my goal is possible without this idea I had right now.
Edited on 19 September 2016 - 11:20 AM
Sewbacca #3
Posted 19 September 2016 - 01:26 PM
Okay, okay, stop stop, stop stop:
Tables threads and functions are objects, booleans, numbers and strings are values.
Object means, that editing the values of a table, edits the object, doesn't matter, which variable pointing to that table.
To illustrate the point:

var1 = {}
var2 = var1 -- var1 points to the same object like var2

var1.hello = 'Hello world'
print(var2.hello) --> Hello world

To copy the values and objects of _G into a new table, you have to iterate through _G and copying the values into Sandbox,
but there is a better possibility:

Sandbox = setmetatable({}, {
  __index = _G, -- inherit all values from _G (but just read only [note that editing Sandbox.fs or any other object will edit _G.fs or any other object (also if you use the first mentioned option) Sandbox.<name> = 'will change just the value in Sandbox NOT in _G'])
  -- __newindex = function(_, key, value) _G[key] = value end, would brake read only
  __metatable = 'Attempt to access protected metatable' -- prevent editing metatable and getting the real _G
})
I think, for your uses, metatables would be enough.

Sewbacca
Edited on 19 September 2016 - 11:29 AM
KingofGamesYami #4
Posted 19 September 2016 - 01:27 PM
You should be able to use a local variable and simply pass a custom environment to the program. You should be doing that anyway; it's the easiest way to make sure overrides (such as fs) are applied to programs, but not your own files.
Piorjade #5
Posted 19 September 2016 - 01:30 PM
Uuum there is a problem with reading you
 section you wrote, it's kinda broken XD
Sewbacca #6
Posted 19 September 2016 - 01:32 PM
Uuum there is a problem with reading you
 section you wrote, it's kinda broken XD
What do you mean?
Piorjade #7
Posted 19 September 2016 - 01:38 PM
You should be able to use a local variable and simply pass a custom environment to the program. You should be doing that anyway; it's the easiest way to make sure overrides (such as fs) are applied to programs, but not your own files.

Yeah but what I already tried it.
As I made currentUsr, for example, local only (not _G) the program I was executing with loadfile() could read from it AND edit it, leading to rootaccess for all next programs which get executed.

Using os.run() didn't help (I already knew it, but hey I just wanted to see what happens) because either I edit the FS API and os.run() gives an error, probably because the programs are located in usr/bin OR I DON'T edit FS API and os.run() works, but the program has full access :/

Okay, okay, stop stop, stop stop:
Tables threads and functions are objects, booleans, numbers and strings are values.
Object means, that editing the values of a table, edits the object, doesn't matter, which variable pointing to that table.
To illustrate the point:

var1 = {}
var2 = var1 -- var1 points to the same object like var2

var1.hello = 'Hello world'
print(var2.hello) --> Hello world

To copy the values and objects of _G into a new table, you have to iterate through _G and copying the values into Sandbox,
but there is a better possibility:

Sandbox = setmetatable({}, {
  __index = _G, -- inherit all values from _G (but just read only [note that editing Sandbox.fs or any other object will edit _G.fs or any other object (also if you use the first mentioned option) Sandbox.<name> = 'will change just the value in Sandbox NOT in _G'])
  -- __newindex = function(_, key, value) _G[key] = value end, would brake read only
  __metatable = 'Attempt to access protected metatable' -- prevent editing metatable and getting the real _G
})
I think, for your uses, metatables would be enough.

Sewbacca

Wait maybe your codesection is not broken, but I don't really understand it as I never used metatables.

Could you give me an example on my code in the first post (how to fix it, so the program cannot edit currentUsr and FS API, neither _G and is limited to the program)
Edited on 19 September 2016 - 11:34 AM
Sewbacca #8
Posted 19 September 2016 - 01:39 PM
Erm…
Now i see:
loadfile(sPath, tEnv)
You can't handle sMode and sName, so:
loadfile("/usr/bin/"..command, SandBox)

but it won't work anyway, see my first post.
Edited on 19 September 2016 - 11:39 AM
KingofGamesYami #9
Posted 19 September 2016 - 01:39 PM
You should loadfile the program, then use setfenv on the returned function to limit the environment.


local func = loadfile( "somefile" ) --#IIRC, you can specify an environment as the second argument, but I might be mistaken
setfenv( func, tLimitedEnv ) --#tLimitedEnv should include pretty much everything in _G, though you might override a couple things
local ok, err = pcall( func ) --# or whatever you want to do with it
Sewbacca #10
Posted 19 September 2016 - 01:42 PM
You should loadfile the program, then use setfenv on the returned function to limit the environment.


local func = loadfile( "somefile" ) --#IIRC, you can specify an environment as the second argument, but I might be mistaken
setfenv( func, tLimitedEnv ) --#tLimitedEnv should include pretty much everything in _G, though you might override a couple things
local ok, err = pcall( func ) --# or whatever you want to do with it
Yes, but setfenv won't be included in Lua 5.2 =(.
Also using the second argument of loadfile would be cleaner and instead of copying all values of _G into tLimitiedEnv you can use metatables.
Edited on 19 September 2016 - 11:44 AM
Piorjade #11
Posted 19 September 2016 - 01:51 PM
You should loadfile the program, then use setfenv on the returned function to limit the environment.


local func = loadfile( "somefile" ) --#IIRC, you can specify an environment as the second argument, but I might be mistaken
setfenv( func, tLimitedEnv ) --#tLimitedEnv should include pretty much everything in _G, though you might override a couple things
local ok, err = pcall( func ) --# or whatever you want to do with it

But can't the running program replace the sandboxed variables/functions/whatever (such as currentUsr to get rootaccess) as long as it runs?
That's the problem here :/

EDIT: So how I understand your example of metatables now (correct me if I'm wrong pls):

we got our _G

we setup a new enviroment (metatable, named SandBox), with an empty table

as a "fallback", if the called var / function is not available by the program, we setup (as the 2nd argument in setmetatable) __index to redirect to _G?
But as you have shown, editing in __index would be possible and so we end up having rootaccess again?
Sry if I sound dumb lol I really never used them :P/>
Edited on 19 September 2016 - 11:55 AM
H4X0RZ #12
Posted 19 September 2016 - 02:01 PM
You should loadfile the program, then use setfenv on the returned function to limit the environment.


local func = loadfile( "somefile" ) --#IIRC, you can specify an environment as the second argument, but I might be mistaken
setfenv( func, tLimitedEnv ) --#tLimitedEnv should include pretty much everything in _G, though you might override a couple things
local ok, err = pcall( func ) --# or whatever you want to do with it

But can't the running program replace the sandboxed variables/functions/whatever (such as currentUsr to get rootaccess) as long as it runs?
That's the problem here :/

EDIT: So how I understand your example of metatables now (correct me if I'm wrong pls):

we got our _G

we setup a new enviroment (metatable, named SandBox), with an empty table

as a "fallback", if the called var / function is not available by the program, we setup (as the 2nd argument in setmetatable) __index to redirect to _G?
But as you have shown, editing in __index would be possible and so we end up having rootaccess again?
Sry if I sound dumb lol I really never used them :P/>/>

There are two ways you could handle this:

1) make the programs env read-only and stop it from overriding the values

2) run the program in a custom env (which is a copy of _G, but NOT _G) and don't care about what happens inside the env. If the program overrides something, it only happens for said program, and once the program dies, you get rid of the env.

I feel like creating a small library which does this stuff for you now, actually so more people start using custom environments (I love them).
Piorjade #13
Posted 19 September 2016 - 02:04 PM
Nvm editing in __index will edit the var/function in __index, but not in _G



But that still ends up having rootacces haha :D/>

Well I guess I have to use that and deal with it, because it looks like there is NO way to restrict programs from editing specific variables :P/>
Would be nice tho.

You should loadfile the program, then use setfenv on the returned function to limit the environment.


local func = loadfile( "somefile" ) --#IIRC, you can specify an environment as the second argument, but I might be mistaken
setfenv( func, tLimitedEnv ) --#tLimitedEnv should include pretty much everything in _G, though you might override a couple things
local ok, err = pcall( func ) --# or whatever you want to do with it

But can't the running program replace the sandboxed variables/functions/whatever (such as currentUsr to get rootaccess) as long as it runs?
That's the problem here :/

EDIT: So how I understand your example of metatables now (correct me if I'm wrong pls):

we got our _G

we setup a new enviroment (metatable, named SandBox), with an empty table

as a "fallback", if the called var / function is not available by the program, we setup (as the 2nd argument in setmetatable) __index to redirect to _G?
But as you have shown, editing in __index would be possible and so we end up having rootaccess again?
Sry if I sound dumb lol I really never used them :P/>/>

There are two ways you could handle this:

1) make the programs env read-only and stop it from overriding the values

2) run the program in a custom env (which is a copy of _G, but NOT _G) and don't care about what happens inside the env. If the program overrides something, it only happens for said program, and once the program dies, you get rid of the env.

I feel like creating a small library which does this stuff for you now, actually so more people start using custom environments (I love them).

Yeah but how can I make a env read-only so I can't just write:
currentUsr = "root"
currentPw = "fuck off I copied the password from .rootpw"

or even:
fs.open = function(shrekt, m8)
–blah
end

:D/> ?

EDIT:
LOL I just thought about having os.lua skim through the whole code of a program and search for something like "fs.open = ", but I think that would be too dumb :P/>
Edited on 19 September 2016 - 12:06 PM
KingofGamesYami #14
Posted 19 September 2016 - 02:13 PM
Literally don't add .rootpw to the env it's running in. If it can't get the password in the first place, it won't be able to set it properly.

Also, currentUsr and currentPw should be local to your script, and you script (and functions defined in your script) shouldn't care what currentUsr and currentPw are in the disposable environment it's running in.

Yes, there are numerous ways to break out of the sandbox. I know many of them. Replacing getfenv and getmetatable will probably be necessary, but again not difficult since you have a disposable environment.
Sewbacca #15
Posted 19 September 2016 - 02:30 PM
You should loadfile the program, then use setfenv on the returned function to limit the environment.


local func = loadfile( "somefile" ) --#IIRC, you can specify an environment as the second argument, but I might be mistaken
setfenv( func, tLimitedEnv ) --#tLimitedEnv should include pretty much everything in _G, though you might override a couple things
local ok, err = pcall( func ) --# or whatever you want to do with it

But can't the running program replace the sandboxed variables/functions/whatever (such as currentUsr to get rootaccess) as long as it runs?
That's the problem here :/

EDIT: So how I understand your example of metatables now (correct me if I'm wrong pls):

we got our _G

we setup a new enviroment (metatable, named SandBox), with an empty table

as a "fallback", if the called var / function is not available by the program, we setup (as the 2nd argument in setmetatable) __index to redirect to _G?
But as you have shown, editing in __index would be possible and so we end up having rootaccess again?
Sry if I sound dumb lol I really never used them :P/>

You are not dumb, just I can't explain things clearly.

To access the metatable, you have to type
getmetatable(tab)
to prevent that,
getmetatable returns the value of __metatable or the real metatable if __metatable = nil
and
setmetatable() fails if __metatable ~= nil

AND

__index is minimal slower than a copy of _G, but creating a copy of _G is slow and take more RAM. You have to decide between the two options (see it clear in the post of HXORZ).

AND

I think you have understood metatables =)
Edited on 19 September 2016 - 12:30 PM
Piorjade #16
Posted 19 September 2016 - 02:33 PM
Literally don't add .rootpw to the env it's running in. If it can't get the password in the first place, it won't be able to set it properly.

Also, currentUsr and currentPw should be local to your script, and you script (and functions defined in your script) shouldn't care what currentUsr and currentPw are in the disposable environment it's running in.

Yes, there are numerous ways to break out of the sandbox. I know many of them. Replacing getfenv and getmetatable will probably be necessary, but again not difficult since you have a disposable environment.

Ok I think I end up coding the root-check directly into os.lua before the program even starts.
The thing is you could probably end up rewriting the FS API for the specific program you are running, but as many solutions you guys posted here are, I think you can rewrite the FS API for the running program anyway…

So I'll be using a sandboxed enviroment with limited FS API (if root isn't logged in or sudo isnt used) and simply won't care if a specific program rewrites FS API until I find / someone posts a 100% read-only solution for enviroments hehe :D/>
Sewbacca #17
Posted 19 September 2016 - 02:49 PM

local tTables = {}
local mt = {
	__metatable = 'Attempt to get protected metatable'
}
mt.__index = function (self, key)
	local var = tTables[self][key]
	if type(var) == 'table' then
		setmetatable({}, mt)
	end
	return var
end
local function createReadOnly (tab)
	if type(tab) ~= 'table' then
		error('table expected, got ' .. type(tab), 2)
	end
	local self = setmetatable({}, mt)
	tTables[self] = tab
	return self
end

This should be 100% secure, but here is a solution without metatables:


-- (C) Sewbacca Technologies --

-- Shortcut headers

local clone;
local handleIs;

-- Shortcuts


handleIs = function (sType, value)
    if type(value) ~= sType then
        error(sType .. ' expected, got ' .. type(value), 3)
    end
end


clone = function (tTable, tExcludes)
    if type(tTable) == 'table' then
        if tExcludes[tTable] then
            return tExcludes[tTable]
        end
        local tCopy = {}
        tExcludes[tTable] = tCopy
        for key, value in pairs(tTable) do
            tCopy[cleanup(key, tExcludes)] = cleanup(value, tExcludes)
        end
        return tCopy
    end
    return tTable
end

-- Install new Table function

table.clone = function (tTable)
    handleIs('table', tTable)
    return clone(tTable, {})
end

table.copy = function (tTable)
    local tRes = {}
    for key, value in pairs(tTable) do
        tRes[key] = value
    end
    return tRes
end

-- End

[Have shorted the code]
Piorjade #18
Posted 19 September 2016 - 03:01 PM
So basically I need to enter:


local SandBox = createReadOnly(_G)

And then I got SandBox, which has things like the edited FS API, and I can NOT write this in the sandbox? :


fs.open = function(a, B)/>/>
--grant me free editing without root
end

Correct me, if I misunderstood how to use createReadOnly()

EDIT:
Tried it the way I thought it would work, it indeed created an enviroment and I could start programs with limited FS API.
But that's not what I meant with "100% secure"
What I really meant with that:
- the program is not allowed to override fs api (even for itself, but I think that is impossible)
- the program is not allowed to override currentUsr (again, even for itself)

What your solution does:
- it indeed makes an enviroment out of the given argument
- it kinda protects the original, where my OS is running on, BUT
- if I set _G.blabla (for example _G.currentUsr), the OS has it taken over, I need to test if I can reset it after the program finishes
- you can still override the things brought from _G to SandBox inside the SandBox and that way get root for that instance

But thank you for the solution, it successfully copies over my _G to a new env, which I can use for programs

I think I really program the OS that way that root-access is checked before running and that FS (and/or currentUsr) overrides are only for that one instance.
Thank you all for trying to help me, it would be awesome if there was the possibility to completely restrict rewriting a var/str/function even inside the env (it's probably impossible, that would be hilarious. Correct me if I'm wrong, lol.)
Edited on 19 September 2016 - 01:47 PM
Sewbacca #19
Posted 19 September 2016 - 04:19 PM
Sorry, but if you want a really read only and not just protect the origin, then you have to override rawset and set a metamethod __newindex = function() error('try to write read only environment', 2) end.
You have to override rawset, because if someone does rawset(_ENV, 'key', 'value'), because he can bypass the __newindex metatamehod.
try:

local tTables = {}
local tReadOnly = {}
local mt = {
		__metatable = 'Attempt to get protected metatable',
		__newindex = function() error('Try to write to read only table', 2) end
}
mt.__index = function (self, key)
		local var = tTables[self][key]
		if type(var) == 'table' then
				setmetatable({}, mt)
		end
		return var
end
local native_rawset = rawset
_G.rawset = function (tab, key, value)
		if tReadOnly[tab] then error('Try to write to read only table', 2) end
		return native_rawset(tab, key, value)
end
local function createReadOnly (tab)
		if type(tab) ~= 'table' then
				error('table expected, got ' .. type(tab), 2)
		end
		local self = setmetatable({}, mt)
		tTables[self] = tab
		tReadOnly[self] = true
		return self
end
It should work (untested).
Edited on 19 September 2016 - 02:41 PM
Sewbacca #20
Posted 19 September 2016 - 04:40 PM
So basically I need to enter:


local SandBox = createReadOnly(_G)

And then I got SandBox, which has things like the edited FS API, and I can NOT write this in the sandbox? :


fs.open = function(a, B)/>/>/>
--grant me free editing without root
end

Correct me, if I misunderstood how to use createReadOnly()

EDIT:
Tried it the way I thought it would work, it indeed created an enviroment and I could start programs with limited FS API.
But that's not what I meant with "100% secure"
What I really meant with that:
- the program is not allowed to override fs api (even for itself, but I think that is impossible)
- the program is not allowed to override currentUsr (again, even for itself)

What your solution does:
- it indeed makes an enviroment out of the given argument
- it kinda protects the original, where my OS is running on, BUT
- if I set _G.blabla (for example _G.currentUsr), the OS has it taken over, I need to test if I can reset it after the program finishes
- you can still override the things brought from _G to SandBox inside the SandBox and that way get root for that instance

But thank you for the solution, it successfully copies over my _G to a new env, which I can use for programs

I think I really program the OS that way that root-access is checked before running and that FS (and/or currentUsr) overrides are only for that one instance.
Thank you all for trying to help me, it would be awesome if there was the possibility to completely restrict rewriting a var/str/function even inside the env (it's probably impossible, that would be hilarious. Correct me if I'm wrong, lol.)

Maybe i have to read the full post.
It should also protect_G.foo = function … and _G._G._G._G._G._G._G._G._G._G._G._G._G._G._G._G.fs.open = function …
Piorjade #21
Posted 19 September 2016 - 05:49 PM
Haha no problem :D/>
Going to test this out as soon as my power goes back on lol
If it works, you will be my lord &amp; saviour :D/>

EDIT:
Oh my god this really works.
You dude just saved me unnecessary workarounds.
EDIT2:
(optional) is it possible with this to protect only specific things in the env ? While testing I noticed that if the program makes own variables (something that's not really coming from the OS, I mean if a program says :
myvariable="hello"
) not local, the env denies to save them. If it says "local" before that everything works just fine. Though many programs made by people already prefer local so that's not really needed.
EDIT3:
Nvm I tried to read your code precisely and it looks to me that you really need local, well I don't really care at least it works, thank you
Edited on 19 September 2016 - 04:47 PM
Sewbacca #22
Posted 19 September 2016 - 07:21 PM
Piorjade said:
EDIT3:
Nvm I tried to read your code precisely and it looks to me that you really need local, well I don't really care at least it works, thank you
Erm… it is possible, I just edited the code:

local tTables = {}
local tReadOnly = {}
local native_rawset = rawset
local mt = {
		__metatable = 'Attempt to get protected metatable',
		__newindex = function(self, key, value)
			if not tTables[self][key] then
				native_rawset(self, key, value)
				return
			end
			error('Try to write to read only table', 2)
		end
}
mt.__index = function (self, key)
		local var = tTables[self][key]
		if type(var) == 'table' then
				setmetatable({}, mt)
		end
		return var
end

_G.rawset = function (tab, key, value)
		if tReadOnly[tab] then error('Try to write to read only table', 2) end
		return native_rawset(tab, key, value)
end

local function createReadOnly (tab)
		if type(tab) ~= 'table' then
				error('table expected, got ' .. type(tab), 2)
		end
		local self = setmetatable({}, mt)
		tTables[self] = tab
		tReadOnly[self] = true
		return self
end
It should work (untested).
Piorjade #23
Posted 19 September 2016 - 07:47 PM
Oh then I was just too dumb to read it correctly, I see it now lol. :D/>

Thanks again and sorry that you have to help me all the time :P/>
Sewbacca #24
Posted 19 September 2016 - 09:06 PM
Oh then I was just too dumb to read it correctly, I see it now lol. :D/>

Thanks again and sorry that you have to help me all the time :P/>

No problem =)