diff options
author | Daniel Silverstone <dsilvers@digital-scurf.org> | 2012-07-29 15:55:41 +0100 |
---|---|---|
committer | Daniel Silverstone <dsilvers@digital-scurf.org> | 2012-07-29 15:55:41 +0100 |
commit | 340c969dabb6d666d5d052ace26c9b656b7a9126 (patch) | |
tree | 63f48eefc5f0ec3c18a7bb64481b06ad6add3384 | |
parent | 7712b97c6ce3d0ccc4260586d7706e29d5e9a77f (diff) | |
download | supple-340c969dabb6d666d5d052ace26c9b656b7a9126.tar.gz |
SANDBOX: Enough sandboxing to get us further. Testing the wrapper is hard
-rw-r--r-- | Makefile | 5 | ||||
-rw-r--r-- | lib/supple.lua | 2 | ||||
-rw-r--r-- | lib/supple/capi.c | 88 | ||||
-rw-r--r-- | lib/supple/sandbox.lua | 128 | ||||
-rw-r--r-- | notes/design | 9 | ||||
-rw-r--r-- | src/wrapper.c | 63 | ||||
-rw-r--r-- | test/test-supple.lua | 6 | ||||
-rw-r--r-- | test/test-supple.sandbox.lua | 52 |
8 files changed, 334 insertions, 19 deletions
@@ -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) |