From 736b14eec970c14ebe64db5dd07c3e67fe27d040 Mon Sep 17 00:00:00 2001 From: Vexatos Date: Wed, 16 Jun 2021 14:18:24 +0200 Subject: [PATCH] Made pasting more safe Pasting malicious code can no longer freeze the program. Using BogoMIPS calculator from OpenComputers to estimate IPS. --- src/sandbox_utils.lua | 103 ++++++++++++++++++++++++++++++++++++++++ src/scenes/loading.lua | 5 ++ src/serialize.lua | 6 ++- src/tools/selection.lua | 2 +- 4 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 src/sandbox_utils.lua diff --git a/src/sandbox_utils.lua b/src/sandbox_utils.lua new file mode 100644 index 00000000..57c2fbf7 --- /dev/null +++ b/src/sandbox_utils.lua @@ -0,0 +1,103 @@ +-- Based on microbenchmark by asiekierka in OpenComputers + +local jit = require("jit") + +local utils = {} + +local hookInterval +local ipsCount + +local function calcIpsCount(hookInterval) + -- Disable JIT optimizations on the current function. + -- Required because LuaJIT would otherwise optimize + -- the "infinite" loop and make it actually infinite. + jit.off(true) + + local bogomipsDivider = 0.05 + local bogomipsDeadline = love.timer.getTime() + bogomipsDivider + local ipsCount = 0 + local bogomipsBusy = true + + local function calcBogoMips() + ipsCount = ipsCount + hookInterval + if love.timer.getTime() > bogomipsDeadline then + bogomipsBusy = false + end + end + + -- The following is a bit of nonsensical-seeming code attempting + -- to cover Lua's VM sufficiently for the IPS calculation. + local bogomipsTmpA = {{["b"]=3, ["d"]=9}} + local function c(k) + if k <= 2 then + bogomipsTmpA[1].d = k / 2.0 + end + end + + debug.sethook(calcBogoMips, "", hookInterval) + while bogomipsBusy do + local st = "" + for k=2,4 do + st = st .. "a" .. k + c(k) + if k >= 3 then + bogomipsTmpA[1].b = bogomipsTmpA[1].b * (k ^ k) + end + end + end + + debug.sethook() + + return ipsCount / bogomipsDivider +end + +function utils.calcIpsAndHookInterval() + local _hookInterval = 1000 + local _ipsCount = calcIpsCount(_hookInterval) + + -- Since our IPS might still be too generous (hookInterval needs to run at most + -- every 0.05 seconds), we divide it further by 10 relative to that. + _hookInterval = (_ipsCount * 0.005) + + if _hookInterval < 1000 then _hookInterval = 1000 end + + ipsCount = _ipsCount + hookInterval = _hookInterval + + return _ipsCount, _hookInterval +end + +local tooLongWithoutYielding = setmetatable({}, { __tostring = function() return "too long without yielding" end}) + +-- Timeout code adapted from OpenCompuers +function utils.pcallWithTimeout(f, timeout, ...) + if not timeout then + return pcall(f, ...) + end + + -- Disable JIT optimization so that debug hooks work + jit.off(f, true) + + -- Emergency initialization if needed + if not hookInterval then + utils.calcIpsAndHookInterval() + end + + local deadline = math.huge + local function checkDeadline() + if love.timer.getTime() > deadline then + error(tooLongWithoutYielding) + end + end + + deadline = love.timer.getTime() + timeout + + debug.sethook(checkDeadline, "", hookInterval) + -- Very naïve but better than nothing + local res = {pcall(f, ...)} + debug.sethook() + + return unpack(res) +end + +return utils \ No newline at end of file diff --git a/src/scenes/loading.lua b/src/scenes/loading.lua index abbe9732..85261a5b 100644 --- a/src/scenes/loading.lua +++ b/src/scenes/loading.lua @@ -32,6 +32,8 @@ end function loadingScene:firstEnter() local tasks = require("task") + local sandboxUtils = require("sandbox_utils") + local sceneHandler = require("scene_handler") local toolHandler = require("tool_handler") local entities = require("entities") @@ -57,6 +59,9 @@ function loadingScene:firstEnter() self:setText(language.scenes.loading.loading) + -- Microbenchmark to estimate instructions per second + sandboxUtils.calcIpsAndHookInterval() + -- Load assets and entity, trigger, effect etc modules tasks.newTask( function() diff --git a/src/serialize.lua b/src/serialize.lua index 0c46ca80..f67aefc6 100644 --- a/src/serialize.lua +++ b/src/serialize.lua @@ -1,6 +1,8 @@ -- TODO - Allow passing around settings table instead of using args? -- Makes it easier to have "profiles" for serializing +local sandboxUtils = require("sandbox_utils") + local serialize = {} local keywords = { @@ -270,14 +272,14 @@ function serialize.serialize(t, pretty, sortKeys, useMetaKeys, seen, depth, succ return success, "{" .. newline .. content.. newline .. closingPadding .. "}" end -function serialize.unserialize(s, safe) +function serialize.unserialize(s, safe, timeout) local func = assert(loadstring("return " .. s)) if safe ~= false then setfenv(func, {}) end - return pcall(func) + return sandboxUtils.pcallWithTimeout(func, timeout) end return serialize \ No newline at end of file diff --git a/src/tools/selection.lua b/src/tools/selection.lua index 452b95f9..4677d3ed 100644 --- a/src/tools/selection.lua +++ b/src/tools/selection.lua @@ -448,7 +448,7 @@ local function pasteItemsHotkey() local clipboard = love.system.getClipboardText() if validateClipboard(clipboard) then - local success, fromClipboard = utils.unserialize(clipboard) + local success, fromClipboard = utils.unserialize(clipboard, true, 3) if success then newPreviews = fromClipboard