-- 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
--

--- Running code in sandboxes
--
-- This module is used by the sandbox code itself to start running sandboxed
-- Lua.  The only entry point is invoked by the sandbox C wrapper during
-- startup.  From here the sandbox locks itself down and then begins to listen
-- for work to do.
--
-- You should only need to interact with this module if you are writing your
-- own Supple wrapper binary to use instead of the provided wrapper.
--

local capi = require 'supple.capi'
local objects = require 'supple.objects'
local comms = require 'supple.comms'
local luxio = require 'luxio'
local sio = require 'luxio.simple'
local track = require 'supple.track'

local loadstring = loadstring
local load = load
local setfenv = setfenv
local gc = collectgarbage
local unpack = unpack
local xpcall = xpcall
local traceback = debug.traceback

local function set_limits(ltab)
   local count = ltab.count
   local memory = ltab.memory
   local limits = { count = count, memory = memory}
   if limits.memory then
      -- Bump memory limit by current usage to be kinder
      limits.memory = limits.memory + (gc "count")
   end
   if not limits.count and not limits.memory then
      return false, "Expected an opcode count or total memory limit"
   end
   comms.set_limits(limits)
   return true
end

local function _wrap(fn, src, globs, dont_wrap_globs)
   local fn_ret, msg

   if not dont_wrap_globs then
      globs = setmetatable({}, { __index = globs, __metatable = true })
   end

   assert(fn, "No function/source provided?")
   assert(src, "No source name provided?")

   if setfenv then
      -- Lua 5.1 style load...
      fn_ret, msg = ((capi.rawtype(fn) == "string") and loadstring or load)(fn, src)
      if not fn_ret then
	 return nil, msg
      end
      setfenv(fn_ret, globs)
   else
      -- Lua 5.2 style load...
      fn_ret, msg = load(fn, src, "t", globs)
      if not fn_ret then
	 return nil, msg
      end
   end

   assert(fn_ret, "Unusual, missing fn_ret now?")

   return fn_ret, globs
end

local function wrapped_unpack(t)
   local packed = t
   if capi.rawtype(t) ~= "table" then
      local len = #t
      packed = {}
      for i = 1, len do
	 packed[i] = t[i]
      end
   end
   return unpack(packed)
end

--- Start the sandbox running
--
-- This routine is invoked by the sandbox wrapper C code and starts the sandbox
-- running.  Approximately it locks the sandbox down, including various limits
-- such as chroot, rlimits, dropping privileges, and seccomp mode if available.
-- Then it begins the main RPC loop for the sandbox.
--
-- @function run
local function run()
   -- Run the sandbox
   local result, errno = capi.lockdown()

   if result ~= "ok" and 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

   if result == "ok" then
      -- Note, if result is "OK" then we're pretty hard jailed
      -- and cannot even do this test lest we get killed
      -- Check that we're definitely solidly jailed
      fh, errno = sio.open("testfile", "rw")
      if fh then
	 fh:close()
	 luxio.unlink("testfile")
	 return 1
      end
   end
   
   -- Prepare a severely limited sandbox
   local sandbox_globals = {
      type = capi.type,
      pairs = capi.pairs,
      ipairs = capi.ipairs,
      next = capi.next,
      unpack = wrapped_unpack,
   }

   for _, k in ipairs({ "table", "string", "pcall",
			 "xpcall", "tostring", "tonumber", "math",
			 "coroutine", "select", "error", "assert" }) do
      sandbox_globals[k] = _G[k]
   end
   -- Complete its "globals"
   sandbox_globals._G = sandbox_globals

   local _go_str = [[
	 return ({...})[1]()
   ]]

   local fn, globs = _wrap(_go_str, "sandbox", sandbox_globals)
   if not fn then
      return 1
   end

   objects.set_name(("supple-sandbox[%d,%%d]"):format(luxio.getpid()))
   objects.set_proc_call(comms.call)

   local dont_wrap_globs = true
   local function wrappered_load(str, name)
      local dwg = dont_wrap_globs
      dont_wrap_globs = false
      return _wrap(str, name, sandbox_globals, dwg)
   end

   -- Pretend we've "given" the host an object called 'supple:loadstring'
   -- which is the loadstring/load function
   track.start()
   objects.give(set_limits, "supple:set_limits")
   objects.give(wrappered_load, "supple:loadstring")
   objects.give(objects.clean_down, "supple:clean_down")
   comms._set_fd(0)

   return fn(comms._wait)
end

local function wrapper_run()
   local ok, retcode = xpcall(run, traceback)
   if not ok then
      error(retcode)
   end
   return retcode
end

return {
   run = wrapper_run,
}
