sol2 icon indicating copy to clipboard operation
sol2 copied to clipboard

how to make sol::thread running in sandbox

Open etorth opened this issue 3 years ago • 0 comments
trafficstars

I am a lua newbie.

This is not a bug report nor a feature request. The problem maybe simple for you guys but really drive me nuts, that how to make sol::thread run in sandbox.

I think many people trying to figure out this in a gentle way, as following links: https://blog.rubenwardy.com/2020/07/26/sol3-script-sandbox/ https://stackoverflow.com/a/24358483/1490269 https://stackoverflow.com/questions/57030514/changing-the-env-of-a-lua-thread-using-c-api

Let me re-state it: We have one lua state to interact with thousands of senders:

sol::state lua;
lua.open_libraries();
lua.script_file("user_defined_functions_and_variables.lua");

create_thousands_of_threads(lua); // one thread per sender
while(!quit){
    auto [event, event_sender] = wait_event();
    auto &th = find_thread(lua, event_sender);
    th.resume(event);
}

The lua state owns thousands of thread/coroutine, but this event-driven is sequential, means thread/coroutine will sequentially get the event to drive the yield/resume.

And each thread/coroutine yield/resume with their own environment, means:

  • Every thread/coroutine has a localized global variable table _G_sandbox, and it should override the default _G.
  • When thread/coroutine read-accesses a global variable, it firstly try to find in the _G_sandbox, if not find then try to find in _G, if still not find, return nil.
  • When thread/coroutine write-access to a global variable, it firstly try to find in the _G_sandbox, access it if found, else
    • if the accessing function is user-defined, create new global variable in _G_sandbox.
    • if this access is from lua standard libraries, report error and crash out. (optional if hard to implement)

When you finish the read, you know I want each thread/coroutine to run in sandbox that won't interfere with each other. They can read-access existing global variables, but shall not write/modify it, alternatively it should be in a localized _G_sandbox.

I currently have an implementation, looks working (or has bug I am not aware of), question is:

  • is this the supposed way for sol2 to implement it?
  • it uses raw lua functions lua_rawgeti/lua_rawseti not through sol2 interface, can this mess up any sol2 internal states?
  • Do we have better/simplier way to implement with sol2?
#include<bits/stdc++.h>
#include "sol/sol.hpp"

void checkError(const sol::protected_function_result &pfr)
{
    if(pfr.valid()){
        return;
    }

    const sol::error err = pfr;
    std::stringstream errStream(err.what());

    std::string errLine;
    while(std::getline(errStream, errLine, '\n')){
        std::cout << "callback error: " << errLine << std::endl;
    }
}

struct LuaThreadRunner
{
    sol::thread runner;
    sol::coroutine callback;

    LuaThreadRunner(sol::state &lua, const std::string &entry)
        : runner(sol::thread::create(lua.lua_state()))
        , callback(sol::state_view(runner.state())[entry])
    {}
};

int main()
{
    sol::state lua;
    lua.open_libraries();

    lua.script(R"(
        local _G = _G
        local error = error
        local coroutine = coroutine

        local _G_sandbox = {}
        function clearTLSTable()
            local threadId, inMainThread = coroutine.running()
            if inMainThread then
                error('call clearTLSTable in main thread')
            else
                _G_sandbox[threadId] = nil
            end
        end

        replaceEnv_metatable = {
            __index = function(table, key)
                local threadId, inMainThread = coroutine.running()
                if not inMainThread then
                    if _G_sandbox[threadId] ~= nil and _G_sandbox[threadId][key] ~= nil then
                        return _G_sandbox[threadId][key]
                    end
                end
                return _G[key]
            end,

            __newindex = function(table, key, value)
                local threadId, inMainThread = coroutine.running()
                if inMainThread then
                    _G[key] = value
                else
                    if _G_sandbox[threadId] == nil then
                        _G_sandbox[threadId] = {}
                    end
                    _G_sandbox[threadId][key] = value
                end
            end
        }
    )");

    sol::environment replaceEnv(lua, sol::create);
    replaceEnv[sol::metatable_key] = sol::table(lua["replaceEnv_metatable"]);

    // idea from: https://blog.rubenwardy.com/2020/07/26/sol3-script-sandbox/
    // set replaceEnv as default environment, otherwise I don't know how to setup replaceEnv to thread/coroutine

    lua_rawgeti(lua.lua_state(), LUA_REGISTRYINDEX, replaceEnv.registry_index());
    lua_rawseti(lua.lua_state(), LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS);

    lua.script(R"(
        function coth_main(runner)
            local threadId, mainThread = coroutine.running()
            if mainThread then
                error('coth_main(runner) called in main thread', runner)
            end

            -- test require
            -- require should still work and accesses global variable: package

            local mod = require('io')
            print(mod)

            coroutine.yield()

            counter = 0     -- localized global varible tested
            counterMax = 10 --

            while counter < counterMax do
                print(string.format('runner %d counter %d, you can resume %d more times', runner, counter, counterMax - counter - 1))
                counter = counter + 1
                coroutine.yield()
            end

            clearTLSTable()
        end
    )");

    LuaThreadRunner runner1(lua, "coth_main");
    checkError(runner1.callback(1));

    LuaThreadRunner runner2(lua, "coth_main");
    checkError(runner2.callback(2));

    const auto fnResume = [](auto &callback, int index)
    {
        if(callback){
            checkError(callback());
        }
        else{
            std::cout << "runner " << index << " has exited" << std::endl;
        }
    };

    while(runner1.callback || runner2.callback){
        int event_from = 0;
        std::cout << "wait event from: ";
        std::cin >> event_from;

        switch(event_from){
            case 1: fnResume(runner1.callback, 1); break;
            case 2: fnResume(runner2.callback, 2); break;
            default: std::cout << "no runner " << event_from << std::endl;
        }
    }
    return 0;
}

etorth avatar May 27 '22 21:05 etorth