-- reskate-hostile: a plugin written to break the sandbox, one attack per command. Every attack -- must end with this plugin stopped or refused and the game running as before. `hostile list` -- names them; after one that stops it, `plugin on reskate-hostile` starts it again. local attacks, order = {}, {} local function attack(name, expect, fn) attacks[name] = {expect = expect, fn = fn} table.insert(order, name) end attack("loop", "stopped: longer than 50 ms", function() while true do end end) attack("caught-loop", "stopped: longer than 50 ms, though it catches the error", function() while true do pcall(function() while true do end end) end end) attack("tick-loop", "stopped at the next tick", function() reskate.on("tick", function() while true do end end) end) attack("memory", "stopped: more than 64 MB", function() local t = {} while true do t[#t + 1] = string.rep("x", 8000000) end end) attack("caught-memory", "stopped: more than 64 MB, though it catches the error", function() pcall(function() local t = {} while true do t[#t + 1] = string.rep("y", 8000000) end end) reskate.console.print("ESCAPED: carried on after a memory bomb") end) attack("huge-string", "stopped: more than 64 MB", function() local s = string.rep("x", 100000000) return #s end) attack("buffer", "stopped: more than 64 MB", function() local b = buffer.create(1e9) return buffer.len(b) end) attack("coroutines", "stopped: its time or its memory", function() while true do coroutine.wrap(function() end)() end end) attack("errors", "stopped after 5 errors in 10 s", function() reskate.on("tick", function() error("hostile: an error every tick") end) end) attack("slow", "stopped after 3 s over 2 ms a tick", function() reskate.on("tick", function() local start = os.clock() while os.clock() - start < 0.004 do end end) end) attack("log-flood", "keeps running: 50 of the 1000 lines are logged", function() for i = 1, 1000 do reskate.log("hostile flood " .. i) end end) attack("timers", "keeps running: refused past 1024 timers", function() for i = 1, 2000 do reskate.time.after(600000, function() end) end end) attack("recursion", "keeps running: an error, not a crash", function() local function deep(n) return deep(n + 1) + 1 end return deep(1) end) attack("slow-unload", "an unload handler that never ends: cut off at 20 ms when it is switched off", function() reskate.on("unload", function() while true do end end) reskate.console.print("hostile: now switch it off: plugin off reskate-hostile") end) attack("teleport-spam", "keeps running: two teleports a second, the rest refused", function() local sent = 0 for i = 1, 1000 do if reskate.skater.teleport(vector.create(0, 0, 0)) then sent += 1 end end reskate.console.print("hostile teleport-spam: " .. sent .. " of 1000 sent") end) attack("raycast-flood", "keeps running: refused past 64 raycasts a tick", function() for i = 1, 100000 do reskate.world.raycast(vector.create(0, 100, 0), vector.create(0, -100, 0)) end end) attack("trick-loop", "stopped at the next trick landed", function() reskate.on("trick_landed", function() while true do end end) reskate.console.print("hostile: now land a trick") end) -- Each returns true when that way out worked. local escapes = { {"io", function() return io ~= nil end}, {"loadstring or load", function() return loadstring ~= nil or load ~= nil end}, {"dofile or loadfile", function() return dofile ~= nil or loadfile ~= nil end}, {"package", function() return package ~= nil end}, {"os beyond the clock", function() return os.execute ~= nil or os.getenv ~= nil or os.remove ~= nil or os.exit ~= nil end}, {"changing reskate", function() return pcall(function() reskate.log = nil end) end}, {"rawset on reskate", function() return pcall(rawset, reskate, "x", 1) end}, {"a metatable on reskate", function() return pcall(setmetatable, reskate, {}) end}, {"changing a library", function() return pcall(function() string.rep = nil end) end}, {"the string metatable", function() return pcall(function() getmetatable("").__index = {} end) end}, {"reading outside the package", function() return pcall(reskate.package.read, "../../../Windows/win.ini") end}, {"a junction-style path", function() return pcall(reskate.package.read, "..\\..\\Windows\\win.ini") end}, {"require outside", function() return pcall(require, "C:/Windows/win.ini") end}, {"require up", function() return pcall(require, "../manifest") end}, {"taking a plugin command", function() return pcall(reskate.console.command, "plugin off", "x", function() end) end}, {"taking a ReSkate command", function() return pcall(reskate.console.command, "load", "x", function() end) end}, {"storage without the permission", function() return pcall(reskate.storage.get, "x") end}, } attack("escapes", "keeps running: every way out refused", function() local escaped = 0 for _, try in escapes do local ok, worked = pcall(try[2]) if ok and worked then escaped += 1 reskate.console.print("ESCAPED: " .. try[1]) end end reskate.console.print("hostile escapes: " .. (#escapes - escaped) .. " of " .. #escapes .. " refused") end) reskate.console.command("hostile", "Attacks the plugin sandbox: hostile list, hostile ", function(args) local name = args[1] if not name or name == "list" then for _, n in order do reskate.console.print(n .. ": " .. attacks[n].expect) end return end local found = attacks[name] if not found then reskate.console.print("hostile: there is no attack " .. name .. " (hostile list names them)") return end reskate.console.print("hostile: " .. name .. ", expected: " .. found.expect) found.fn() end) reskate.log("reskate-hostile loaded: hostile list names its attacks")