summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Silverstone <dsilvers@digital-scurf.org>2012-07-29 15:55:41 +0100
committerDaniel Silverstone <dsilvers@digital-scurf.org>2012-07-29 15:55:41 +0100
commit340c969dabb6d666d5d052ace26c9b656b7a9126 (patch)
tree63f48eefc5f0ec3c18a7bb64481b06ad6add3384
parent7712b97c6ce3d0ccc4260586d7706e29d5e9a77f (diff)
downloadsupple-340c969dabb6d666d5d052ace26c9b656b7a9126.tar.gz
SANDBOX: Enough sandboxing to get us further. Testing the wrapper is hard
-rw-r--r--Makefile5
-rw-r--r--lib/supple.lua2
-rw-r--r--lib/supple/capi.c88
-rw-r--r--lib/supple/sandbox.lua128
-rw-r--r--notes/design9
-rw-r--r--src/wrapper.c63
-rw-r--r--test/test-supple.lua6
-rw-r--r--test/test-supple.sandbox.lua52
8 files changed, 334 insertions, 19 deletions
diff --git a/Makefile b/Makefile
index 38043f6..6378a81 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,6 @@
all: test
-LMODULES := supple supple.request supple.objects
+LMODULES := supple supple.request supple.objects supple.sandbox
CMODULES := supple.capi
MODULES := $(LMODULES) $(CMODULES)
LUA_VER := 5.1
@@ -43,6 +43,9 @@ wrapper: src/wrapper.c
testwrapper: src/wrapper.c
$(CC) $(LFLAGS) $(CFLAGS) -DTESTING_SUPPLE -o $@ $< -llua$(LUA_VER)
+ -chown root:root $@
+ -chmod u+s $@
+ ls -l $@
install: build
mkdir -p $(LINST_ROOT)/supple
diff --git a/lib/supple.lua b/lib/supple.lua
index 0d87d20..e74bfa0 100644
--- a/lib/supple.lua
+++ b/lib/supple.lua
@@ -10,6 +10,7 @@
local capi = require 'supple.capi'
local request = require 'supple.request'
local objects = require 'supple.objects'
+local sandbox = require 'supple.sandbox'
local _VERSION = 1
local _ABI = 1
@@ -20,6 +21,7 @@ return {
capi = capi,
request = request,
objects = objects,
+ sandbox = sandbox,
_VERSION = _VERSION,
VERSION = VERSION,
_ABI = _ABI,
diff --git a/lib/supple/capi.c b/lib/supple/capi.c
index c413c20..c0f8f48 100644
--- a/lib/supple/capi.c
+++ b/lib/supple/capi.c
@@ -13,7 +13,12 @@
#include <lua.h>
#include <lauxlib.h>
+#include <sys/types.h>
#include <string.h>
+#include <stdbool.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <errno.h>
/* The API exposed, increment on backward-compatible changes */
#define CAPI_API 1
@@ -157,12 +162,95 @@ supple_capi_rawtype(lua_State *L)
return 1;
}
+/* Lockdown function, takes nothing, returns a string token and errno which
+ * explains the result. We're using SUPPLE_MKDTEMP as a template for
+ * mkdtemp(). The wrapper ensures it is unset unless testing.
+ */
+#define TEMPDIR_NAME ((getenv("SUPPLE_MKDTEMP") != NULL) ? \
+ (getenv("SUPPLE_MKDTEMP")) : ("/var/tmp/suppleXXXXXX"))
+static int
+supple_capi_lockdown(lua_State *L)
+{
+ bool rootly = (geteuid() == 0);
+ char *tempdir;
+ lua_settop(L, 0); /* Discard any arguments */
+
+ /* Prepare a copy of the tempdir template ready */
+ tempdir = lua_newuserdata(L, strlen(TEMPDIR_NAME));
+ strcpy(tempdir, TEMPDIR_NAME);
+ /* And make a temp directory to use */
+ if (mkdtemp(tempdir) != tempdir) {
+ /* Failed to make the temp dir, give up */
+ int saved_errno = errno;
+ lua_pushliteral(L, "mkdir");
+ lua_pushnumber(L, saved_errno);
+ return 2;
+ }
+
+ /* Temporary directory made, if we're rootly, chown it */
+ if (rootly) {
+ if (chown(tempdir, 0, 0) != 0) {
+ int saved_errno = errno;
+ rmdir(tempdir);
+ lua_pushliteral(L, "chown");
+ lua_pushnumber(L, saved_errno);
+ return 2;
+ }
+ }
+
+ /* Change into that directory */
+ if (chdir(tempdir) != 0) {
+ int saved_errno = errno;
+ rmdir(tempdir);
+ lua_pushliteral(L, "chdir");
+ lua_pushnumber(L, saved_errno);
+ return 2;
+ }
+
+ /* Remove the directory */
+ if (rmdir(tempdir) != 0) {
+ int saved_errno = errno;
+ lua_pushliteral(L, "rmdir");
+ lua_pushnumber(L, saved_errno);
+ return 2;
+ }
+
+ /* chroot() to the ephemeral dir */
+ if (rootly && (chroot(".") != 0)) {
+ int saved_errno = errno;
+ lua_pushliteral(L, "chroot");
+ lua_pushnumber(L, saved_errno);
+ return 2;
+ }
+
+ /* Ensure our PWD is "/" */
+ if (rootly && chdir("/") != 0) {
+ int saved_errno = errno;
+ lua_pushliteral(L, "chdir");
+ lua_pushnumber(L, saved_errno);
+ return 2;
+ }
+
+ /* Drop privs */
+ if (rootly && setuid(getuid()) != 0) {
+ int saved_errno = errno;
+ lua_pushliteral(L, "setuid");
+ lua_pushnumber(L, saved_errno);
+ return 2;
+ }
+
+ lua_pushstring(L, (rootly ? "ok" : "oknonroot"));
+ lua_pushnumber(L, 0);
+ return 2;
+}
+
static const struct luaL_Reg
supple_capi_functions[] = {
{ "explain", supple_capi_explain },
{ "new_proxy", supple_capi_new_proxy },
{ "type", supple_capi_type },
{ "rawtype", supple_capi_rawtype },
+ { "lockdown", supple_capi_lockdown },
{ NULL, NULL }
};
diff --git a/lib/supple/sandbox.lua b/lib/supple/sandbox.lua
new file mode 100644
index 0000000..6b2d3ab
--- /dev/null
+++ b/lib/supple/sandbox.lua
@@ -0,0 +1,128 @@
+-- lib/supple/sandbox.lua
+--
+-- Sandbox (for) Untrusted Procedure Partitioning (in) Lua Engine
+--
+-- Code which runs the core sandbox functionality of supple.
+--
+-- This module runs in the sandbox interpreter which means some of the code
+-- runs with root access. As such, we minimise what can be done before
+-- we drop privileges.
+--
+-- The wrapper used to run us already ensured that LUA_PATH etc are
+-- not set, so we don't have to worry about non-system-installed modules
+-- getting in our way. Once the supple libraries are loaded (which will
+-- include loading luxio etc) we're good to go and can ask the supple.capi
+-- module to lock us down.
+--
+-- Copyright 2012 Daniel Silverstone <dsilvers@digital-scurf.org>
+--
+-- For licence terms, see COPYING
+--
+
+local capi = require 'supple.capi'
+local luxio = require 'luxio'
+local sio = require 'luxio.simple'
+
+local load = load
+local setfenv = setfenv
+local type = type
+
+-- Run fn with globs as its globals. Returns a function to run which
+-- returns the return values of fn, and also wrap returns the table
+-- which will be filled with any new globals fn creates.
+--
+-- If fn is a string then it is used as the source of the function, if it
+-- is a function then it is expected to be a closure which when called
+-- repeatedly returns more data loaded (perhaps from a file?). src is
+-- expected to be the name associated with this source code.
+--
+-- In case of error, returns nil, errmsg
+local function _wrap(fn, src, globs)
+ local fn_glob = setmetatable({}, { __index = globs, __metatable=true })
+ local fn_ret, msg
+
+ assert(fn, "No function/source provided?")
+ assert(src, "No source name provided?")
+ globs = globs or {}
+
+ if setfenv then
+ -- Lua 5.1 style load...
+ fn_ret, msg = ((type(fn) == "string") and loadstring or load)(fn, src)
+ if not fn_ret then
+ return nil, msg
+ end
+ setfenv(fn_ret, fn_glob)
+ else
+ -- Lua 5.2 style load...
+ fn_ret, msg = load(fn, src, "t", fn_glob)
+ if not fn_ret then
+ return nil, msg
+ end
+ end
+
+ assert(fn_ret, "Unusual, missing fn_ret now?")
+
+ return fn_ret, fn_glob
+end
+
+local function sandboxed_go()
+ -- Remove ourselves from the globals table so we cannot
+ -- be reentered
+ go = nil;
+
+-- return io.receive()
+ return 0
+end
+
+local function run()
+ -- Run the sandbox
+ local result, errno = capi.lockdown()
+
+ if result ~= "ok"
+-- START_TEST_ONLY
+ and result ~= "oknonroot"
+-- END_TEST_ONLY
+ then
+ -- Failure to sandbox, so abort
+ print(result, luxio.strerror(errno))
+ return errno
+ end
+
+-- START_TEST_ONLY
+ if result ~= "oknonroot" then
+-- END_TEST_ONLY
+ -- Check that we're definitely solidly jailed
+ fh, errno = sio.open("testfile", "rw")
+ if fh then
+ fh:close()
+ luxio.unlink("testfile")
+ return 1
+ end
+-- START_TEST_ONLY
+ end
+-- END_TEST_ONLY
+
+ -- Prepare a severely limited sandbox
+ local sandbox_globals = {}
+
+ for _, k in ipairs({ "table", "string", "pairs", "ipairs", "pcall",
+ "xpcall", "unpack", "tostring", "tonumber", "math",
+ "type", "coroutine", "select", "error", "assert" }) do
+ sandbox_globals[k] = _G[k]
+ end
+ -- Complete its "globals"
+ sandbox_globals._G = sandbox_globals
+ -- And add in the magic function we need
+ sandbox_globals.go = sandboxed_go
+
+ local fn, globs = _wrap("return go()", "sandbox", sandbox_globals)
+ if not fn then
+ return 1
+ end
+
+ return fn()
+end
+
+return {
+ run = run,
+}
diff --git a/notes/design b/notes/design
index d055d1d..032f083 100644
--- a/notes/design
+++ b/notes/design
@@ -158,9 +158,12 @@ injected.
1. The host starts by preparing a socketpair and forking.
2. The forked process dup2()s the socketpair onto fd 0 and force-closes every
FD (regardless of the likelyhood of it being open).
-3. Then the forked process executes a specifically compiled lua interpreter.
-4. The interpreter loads the Supple modules and then the one module so
- instructed by the host.
+3. Then the forked process executes a specifically compiled lua interpreter
+ wrapper program which prevents LUA_PATH et al being passed to the real
+ lua interpreter. It also sets the command line for the real interpreter
+ to simply be: lua -lsupple -esupple.sandbox.run()
+4. The real interpreter then loads the Supple modules and starts the sandbox
+ process.
5. Said interpreter, if setuid(root) then
1. makes a directory owned by root
2. changes into that directory
diff --git a/src/wrapper.c b/src/wrapper.c
index 59b8fd5..7cb52e3 100644
--- a/src/wrapper.c
+++ b/src/wrapper.c
@@ -2,7 +2,7 @@
*
* Sandbox (for) Untrusted Procedure Partitioning (in) Lua Engine - Supple
*
- * Wrapper for Lua interpreter to protect and isolate the sandbox code.
+ * Wrapper interpreter to protect and isolate the sandbox code.
*
* Copyright 2012 Daniel Silverstone <dsilvers@digital-scurf.org>
*
@@ -11,33 +11,66 @@
*/
#include <lua.h>
+#include <lauxlib.h>
+#include <lualib.h>
+
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
-char * const sub_argv[] = {
- LUA_INTERP_NAME,
- "-lsupple",
- "-esupple.sandbox.run()",
- NULL
-};
+typedef struct {
+ int retcode;
+} prot_args;
+
+static int
+protected_main(lua_State *L)
+{
+ prot_args *parg = (prot_args *)lua_touserdata(L, 1);
+
+ luaL_openlibs(L);
+
+ lua_getglobal(L, "require");
+ lua_pushstring(L, "supple");
+ lua_call(L, 1, 1);
+
+ lua_getfield(L, -1, "sandbox");
+ lua_getfield(L, -1, "run");
+
+ lua_call(L, 0, 1);
+
+ if (lua_isnumber(L, -1)) {
+ parg->retcode = lua_tonumber(L, -1);
+ }
+
+ return 0;
+}
int
main(int argc, char **argv)
{
+ prot_args parg;
+ lua_State *L;
+ int success;
+
/* Perform pre-lua-interpreter initialisation */
#ifndef TESTING_SUPPLE
unsetenv(LUA_PATH);
unsetenv(LUA_CPATH);
+ unsetenv("SUPPLE_MKDTEMP");
#endif
unsetenv(LUA_INIT);
-
- /* Now go on to run:
- * /path/to/lua -lsupple -esupple.sandbox.run()
- */
- if (execv(LUA_INTERP_PATH, sub_argv) == -1) {
- perror("execv(" LUA_INTERP_PATH ")");
+
+ L = luaL_newstate();
+ if (L == NULL) {
+ return EXIT_FAILURE;
}
-
- return EXIT_FAILURE;
+
+ parg.retcode = 0;
+
+ success = lua_cpcall(L, &protected_main, &parg);
+
+ lua_close(L);
+
+ return ((success == 0) && (parg.retcode == 0)) ? EXIT_SUCCESS :
+ ((success == 0) ? parg.retcode : EXIT_FAILURE);
}
diff --git a/test/test-supple.lua b/test/test-supple.lua
index fdd08e4..fe7bb9b 100644
--- a/test/test-supple.lua
+++ b/test/test-supple.lua
@@ -14,6 +14,7 @@ local luacov = require 'luacov'
local capi = require 'supple.capi'
local request = require 'supple.request'
local objects = require 'supple.objects'
+local sandbox = require 'supple.sandbox'
local supple = require 'supple'
local testnames = {}
@@ -47,6 +48,11 @@ function suite.objects_passed()
"Supple's objects entry is not supple.objects")
end
+function suite.sandbox_passed()
+ assert(supple.sandbox == sandbox,
+ "Supple's sandbox entry is not supple.sandbox")
+end
+
local count_ok = 0
for _, testname in ipairs(testnames) do
-- print("Run: " .. testname)
diff --git a/test/test-supple.sandbox.lua b/test/test-supple.sandbox.lua
new file mode 100644
index 0000000..c75e6fc
--- /dev/null
+++ b/test/test-supple.sandbox.lua
@@ -0,0 +1,52 @@
+-- test/test-supple.lua
+--
+-- Supple - Tests for the core module
+--
+-- Copyright 2012 Daniel Silverstone <dsilvers@digital-scurf.org>
+--
+-- For Licence terms, see COPYING
+--
+
+-- Step one, start coverage
+
+local luacov = require 'luacov'
+
+local sandbox = require 'supple.sandbox'
+
+local testnames = {}
+
+local real_assert = assert
+local total_asserts = 0
+local function assert(...)
+ local retval = real_assert(...)
+ total_asserts = total_asserts + 1
+ return retval
+end
+
+local function add_test(suite, name, value)
+ rawset(suite, name, value)
+ testnames[#testnames+1] = name
+end
+
+local suite = setmetatable({}, {__newindex = add_test})
+
+function suite.try_run()
+ local ret = sandbox.run()
+ assert(ret == 0, "Run failed?")
+end
+
+local count_ok = 0
+for _, testname in ipairs(testnames) do
+-- print("Run: " .. testname)
+ local ok, err = xpcall(suite[testname], debug.traceback)
+ if not ok then
+ print(err)
+ print()
+ else
+ count_ok = count_ok + 1
+ end
+end
+
+print(tostring(count_ok) .. "/" .. tostring(#testnames) .. " [" .. tostring(total_asserts) .. "] OK")
+
+os.exit(count_ok == #testnames and 0 or 1)