11config = require ( " lapis.config" ) . get!
22
33import eval_rockspec, run_sandboxed, get_sandbox_source, decode_result from require " helpers.rockspec_eval"
4+ import shell_escape from require " lapis.cmd.path"
5+
6+ RESTY = " /usr/local/openresty/bin/resty"
47
58-- busted may run on PUC Lua, but the sandbox always runs on LuaJIT
69bytecode = string.dump -> 1
710
11+ local bwrap_available
812can_bwrap = ->
9- return false unless io.open " /usr/bin/bwrap"
10- status = os.execute " unshare -Ur true > /dev/null 2>&1"
11- status == 0 or status == true
13+ if bwrap_available == nil
14+ status = io.open ( " /usr/bin/bwrap" ) and os.execute " unshare -Ur true > /dev/null 2>&1"
15+ bwrap_available = status == 0 or status == true
16+ bwrap_available
17+
18+ -- CI sets REQUIRE_SANDBOX so a missing dependency fails the build instead of
19+ -- quietly skipping the specs that cover production's configuration
20+ skip = ( reason) ->
21+ assert not os.getenv ( " REQUIRE_SANDBOX" ) , reason
22+ pending reason
23+
24+ bwrap_it = ( name, fn) ->
25+ it name, ->
26+ return skip " bubblewrap is unavailable" unless can_bwrap!
27+ fn!
28+
29+ -- Runs lua with resty, where the child is run with ngx.pipe like it is in
30+ -- the web server. Returns everything it printed
31+ run_resty = ( lua) ->
32+ f = assert io.popen " LAPIS_ENVIRONMENT=test #{RESTY} -I . -e '#{shell_escape lua}' 2>&1"
33+ with f\ read " *a"
34+ f\ close!
35+
36+ resty_eval = ( rockspec) ->
37+ run_resty string.format [[
38+ local config = require("lapis.config").get()
39+ config.rockspec_sandbox = { bwrap = true }
40+ local spec, err = require("helpers.rockspec_eval").eval_rockspec(%q)
41+ io.write("result: ", tostring(spec and spec.package), " ", tostring(err))
42+ ]] , rockspec
1243
1344describe " helpers.rockspec_eval" , ->
1445 local original
@@ -80,18 +111,7 @@ describe "helpers.rockspec_eval", ->
80111 assert . falsy spec
81112 assert . same " Failed to eval rockspec" , err
82113
83- describe " in process" , ->
84- before_each ->
85- config. rockspec_sandbox = nil
86-
87- data_tests!
88-
89- describe " child process" , ->
90- before_each ->
91- config. rockspec_sandbox = {}
92-
93- data_tests!
94-
114+ limit_tests = ( it) ->
95115 it " kills a backtracking pattern" , ->
96116 spec, err = eval_rockspec [[ x = ("a"):rep(30000):find(".-.-.-.-b")]]
97117 assert . falsy spec
@@ -107,6 +127,19 @@ describe "helpers.rockspec_eval", ->
107127 assert . falsy spec
108128 assert . same " Failed to eval rockspec" , err
109129
130+ describe " in process" , ->
131+ before_each ->
132+ config. rockspec_sandbox = nil
133+
134+ data_tests!
135+
136+ describe " child process" , ->
137+ before_each ->
138+ config. rockspec_sandbox = {}
139+
140+ data_tests!
141+ limit_tests it
142+
110143 it " reports a sandbox that can't start" , ->
111144 config. rockspec_sandbox = { luajit : " /nonexistent" }
112145 spec, err = eval_rockspec [[ package = "my-module"]]
@@ -156,23 +189,58 @@ describe "helpers.rockspec_eval", ->
156189 before_each ->
157190 config. rockspec_sandbox = { bwrap : true }
158191
159- it " evaluates a rockspec" , ->
160- unless can_bwrap!
161- pending " bubblewrap is unavailable"
162- return
163-
192+ bwrap_it " evaluates a rockspec" , ->
164193 spec = assert eval_rockspec [[ package = "my-module"]]
165194 assert . same { package : " my-module" } , spec
166195
167- it " hides the site's files" , ->
168- unless can_bwrap!
169- pending " bubblewrap is unavailable"
170- return
196+ limit_tests bwrap_it
171197
198+ bwrap_it " hides the site's files" , ->
172199 site_config = " #{io.popen(" pwd" )\r ead " * l" }/config.moon"
173200 assert io.open site_config
174201
175202 out = run_sandboxed string.format ( [[
176203 io.write(tostring(io.open(%q) ~= nil), " ", tostring(io.open("/etc/passwd") ~= nil))
177204 ]] , site_config) , " "
178205 assert . same " false false" , out
206+
207+ bwrap_it " has no network" , ->
208+ -- a udp connect to 1.1.1.1:53 sends nothing, it only needs a route
209+ out = run_sandboxed [[
210+ local ffi = require("ffi")
211+ ffi.cdef("int socket(int domain, int type, int protocol); int connect(int fd, const void *addr, unsigned int len);")
212+ local addr = ffi.new("uint8_t[16]", {2, 0, 0, 53, 1, 1, 1, 1})
213+ io.write(tostring(ffi.C.connect(ffi.C.socket(2, 2, 0), addr, 16)))
214+ ]] , " "
215+ assert . same " -1" , out
216+
217+ describe " inside nginx" , ->
218+ resty_it = ( name, fn) ->
219+ it name, ->
220+ return skip " resty is unavailable" unless io.open RESTY
221+ fn!
222+
223+ resty_it " evaluates a rockspec under bubblewrap" , ->
224+ return skip " bubblewrap is unavailable" unless can_bwrap!
225+ out = resty_eval [[ package = "my-module"]]
226+ assert . truthy out\ find( " result: my-module nil" , 1 , true ) , out
227+
228+ resty_it " kills a backtracking pattern under bubblewrap" , ->
229+ return skip " bubblewrap is unavailable" unless can_bwrap!
230+ out = resty_eval [[ x = ("a"):rep(30000):find(".-.-.-.-b")]]
231+ assert . truthy out\ find( " result: nil Failed to eval rockspec" , 1 , true ) , out
232+
233+ resty_it " applies the timeout to the whole run" , ->
234+ -- each write restarts a per operation timeout, but not the deadline
235+ out = run_resty [[
236+ local config = require("lapis.config").get()
237+ local out, err = require("helpers.rockspec_eval").run_sandboxed([=[
238+ for i = 1, 20 do
239+ os.execute("/usr/bin/sleep 0.3")
240+ io.write("a")
241+ io.stdout:flush()
242+ end
243+ ]=], "", { timeout = 1 })
244+ io.write("result: ", tostring(err))
245+ ]]
246+ assert . truthy out\ find( " result: timeout" , 1 , true ) , out
0 commit comments