summaryrefslogtreecommitdiff
path: root/lib/supple/host.lua
blob: 0ebe981bd9b8eaa0d411ccc7f78b53e079d97546 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
-- lib/supple/host.lua
--
-- Sandbox (for) Untrusted Procedure Partitioning (in) Lua Engine
--
-- Management of the host side of Supple
--
-- Copyright 2012 Daniel Silverstone <dsilvers@digital-scurf.org>
--
-- For licence terms, see COPYING
--

--- Running the host-side of sandboxed code
--
-- When running code in Supple sandboxes, the starting point is this module.
-- The host application should require supple and then begin from here...
--
--     local supple = require 'supple'
--     print(supple.sandbox.run("return ...", "example code", "arg1", "arg2"))
--     -- Expect: 'arg1    arg2' as output
--


local luxio = require 'luxio'
local subprocess = require 'luxio.subprocess'

local comms = require 'supple.comms'
local objects = require 'supple.objects'
local capi = require 'supple.capi'
local track = require 'supple.track'

local hostname = "host"
local counter = 0
local limits, globals

local function simplify(t, memo)
   if not memo then memo = {} end
   if memo[t] then return memo[t] end
   local ret = {}
   memo[t] = ret
   local kk, vv
   for k, v in capi.pairs(t) do
      kk, vv = k, v
      if capi.type(k) == "table" then
	 kk = simplify(k, memo)
      end
      if capi.type(v) == "table" then
	 vv = simplify(v, memo)
      end
      if capi.rawtype(kk) ~= "userdata" and
	 capi.rawtype(vv) ~= "userdata" then
	 -- We've not got a proxy left over anywhere, so copy it
	 ret[kk] = vv
      end
   end
   return ret
end

local function run_wrapper()
   local wrapperpath = "@@WRAPPER_BIN@@"
   -- START_TEST_SUPPLE
   wrapperpath = os.getenv "SUPPLE_TEST_WRAPPER"
   -- END_TEST_SUPPLE
   local fds = {}
   local ret, errno = luxio.socketpair(luxio.AF_UNIX, luxio.SOCK_STREAM,
				       luxio.PF_UNIX, fds)
   if ret ~= 0 then
      error("Unable to launch subprocess, could not prepare socketpair():"
	    .. luxio.strerror(errno))
   end
   local proc, msg = subprocess.spawn {
      "supple-sandbox",
      exe = wrapperpath,
      stdin = fds[1],
      stdout = -1,
      stderr = -1,
      close_in_child = { fds[1], fds[2] },
   }
   if not proc then
      error(msg)
   end
   luxio.close(fds[1])
   return proc, fds[2]
end

--- Run some code in a Supple sandbox
--
-- Call this routine to run some code in a Supple sandbox.  The code is
-- compiled and run entirely within the sandbox process.  Any kind of value
-- can be passed to the code as an argument.  Intrinsics are transferred
-- immediately (numbers, booleans, nils, strings) but complex values (tables,
-- userdata, and functions) are sent over as a proxy and actions on those
-- values are proxied back to the host to be run.  Users of Supple should be
-- aware that those routines are therefore not sandboxed.
--
-- @tparam string codestr The code to run in the sandbox, as a string.
-- @tparam string codename The name to give to the code running in the sandbox.
-- @param[opt] ... Arguments to pass to the code running in the sandbox.
-- @treturn[1] boolean A value which has 'truth'
-- @return[1] ... The results returned from running the code in the sandbox.
-- @treturn[2] boolean A value which has 'falsehood'
-- @treturn[2] string The basic Lua error message associated with the failure
-- @treturn[2] string The Supple traceback (very large, not useful for users)
-- @function run

local function run_sandbox(codestr, codename, ...)
   -- Prepare and start a sandbox,
   -- compiling the codestr and running it
   -- with the given args
   local child, commsfd = run_wrapper()

   counter = counter + 1
   objects.set_name(("%s[%d,%%d]"):format(hostname,counter))
   comms._set_fd(commsfd)
   objects.set_proc_call(comms.call)

   track.start()
   
   local func, env = comms.call("supple:loadstring", "__call", codestr, codename)
   if not func then
      error(env)
   end

   local limitsok, limitserr = true
   if limits then
      limitsok, limitserr = comms.call("supple:set_limits", "__call", limits)
   end

   -- In a protected manner, capture the output of the call
   local args, ret = {...}
   local function capture()
      ret = {func(unpack(args))}
   end

   local ok, err
   if limitsok then
      if globals then
	 -- Hand over the globals
	 for k, v in pairs(globals) do
	    env[k] = v
	 end
      end
      ok, err = pcall(capture)
   else
      ok, err = limitsok, limitserr
   end
   -- Convert any complex objects returned to us, so we can clean up...
   if ok then
      ret = simplify(ret)
   end
   -- We need to clean up, so dump all the objects
   func = nil
   env = nil
   -- And ask the supple API to clear down too
   objects.clean_down(true)

   comms._set_fd(-1)
   luxio.close(commsfd)
   child:wait()
   if ok then
      return ok, unpack(ret)
   else
      return ok, err, track.stop()
   end
end

---
-- Load a string into the sandbox and return it as a wrappered function.
--
-- This loads the given string (with given name) into the sandbox interpreter
-- and then wrappers the function and returns it to the caller.  This can then
-- be used to call code inside the sandbox.  This is the entry point to load
-- further code into the sandbox and should only be called during running
-- sandboxed code (i.e. `supple.host.run` is running)
--
-- @tparam string codestr The code to be loaded in the sandboxed interpreter
-- @tparam string codename The name to be given to the code when it is loaded
-- @treturn function The wrappered function which can then be called or handed
--                   back to the sandbox as needed.
-- @treturn table    The function environment for the loaded sandboxed code.
-- @function loadstring

local function sandboxed_loadstring(codestr, codename)
   return comms.call("supple:loadstring", "__call", codestr, codename)
end

---
-- Set the name by which the host refers to itself in traces.
--
-- Calling this resets the host sandbox counter and sets the name of the host
-- to the given new name.  This mostly has an effect on object names and trace
-- data which typically only appear if the user of Supple chooses to trace the
-- activity of the sandbox, or if an error occurs.
--
-- @tparam string newname The new host name to set
-- @function set_name

local function set_hostname(newname)
   hostname = newname
   counter = 0
end

---
-- Set a new limits table for use in a new sandbox instance.
--
-- When a sandbox is run (from `supple.host.run`) it has a number of soft
-- limits sent to it to be honoured during the runtime of the sandbox.
-- Normally the sandbox limits itself only by hard limits, but this call allows
-- tighter soft limits to be set if so-desired.
--
-- The table provided takes the form:
--
--     { [count = maxopcodestorun], [memory = maxbytestouse] }
--
-- If you call this function then at least one, but optionally both of the
-- count and memory table entries need to be present.
--
-- @tparam table newlimits The new soft-limits for any new sandboxes
-- @function set_limits

local function set_limits(newlimits)
   limits = newlimits
end

---
-- Set a new globals table to be used by new sandbox runs.
--
-- When a sandboxed function is run, it is run within a given globals table.
-- The globals in question are provided by calling this function.  Remember
-- that the table will be passed *shallowly* and new globals created by the
-- sandboxed code will not be copied back to the host.
--
-- @tparam table newglobals The set of globals to be passed to the sandbox
-- @function set_globals

local function set_globals(newglobals)
   globals = newglobals
end

return {
   run = run_sandbox,
   loadstring = sandboxed_loadstring,
   set_name = set_hostname,
   set_limits = set_limits,
   set_globals = set_globals,
}