From 6cd0c8a414a4803c845c22b015b1b5edb5c4ccbd Mon Sep 17 00:00:00 2001 From: Meo597 <197331664+Meo597@users.noreply.github.com> Date: Thu, 15 Oct 2026 02:28:36 +0800 Subject: [PATCH] lua: preserve native Stringer and error output in sandbox tostring() --- common/lua/program.go | 16 ++++++++++++---- common/lua/program_test.go | 24 +++++++++++++++++++++++- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/common/lua/program.go b/common/lua/program.go index 1078fc4a3..4979b6f09 100644 --- a/common/lua/program.go +++ b/common/lua/program.go @@ -3,6 +3,7 @@ package lua import ( "bytes" "context" + "fmt" "os" "strings" "time" @@ -121,15 +122,22 @@ func newSandboxedState() *glua.LState { L.GetGlobal(glua.StringLibName).(*glua.LTable).RawSetString("dump", glua.LNil) L.GetGlobal(glua.MathLibName).(*glua.LTable).RawSetString("randomseed", glua.LNil) - // Hide object addresses without invoking __tostring. + // Preserve native Stringer/error output without using Lua's address-based defaults. L.SetGlobal("tostring", L.NewFunction(func(L *glua.LState) int { value := L.CheckAny(1) + result := "" switch value.Type() { case glua.LTNil, glua.LTBool, glua.LTNumber, glua.LTString: - L.Push(glua.LString(value.String())) - default: - L.Push(glua.LString("")) + result = value.String() + case glua.LTUserData: + switch native := value.(*glua.LUserData).Value.(type) { + case fmt.Stringer: + result = native.String() + case error: + result = native.Error() + } } + L.Push(glua.LString(result)) return 1 })) diff --git a/common/lua/program_test.go b/common/lua/program_test.go index 1c0664976..4737caa89 100644 --- a/common/lua/program_test.go +++ b/common/lua/program_test.go @@ -8,6 +8,7 @@ import ( "encoding/base64" "encoding/hex" "errors" + "net" "os" "path/filepath" "runtime" @@ -18,6 +19,7 @@ import ( "weak" glua "github.com/yuin/gopher-lua" + luar "layeh.com/gopher-luar" ) func TestProgramStatesAreIndependent(t *testing.T) { @@ -631,7 +633,12 @@ for _, convert in ipairs({tostring, require("_G").tostring}) do assert(convert("") == "" and convert("hello") == "hello") assert(convert(42, {}) == "42") assert(not pcall(convert)) - for _, value in ipairs({{}, function() end, convert, testUserdata, testThread, testChannel}) do + assert(convert(testIPv4) == "192.0.2.1") + assert(convert(testIPv6) == "2001:db8::1") + assert(convert(testNativeIP) == "198.51.100.1") + assert(convert(testNativeStringer) == "0xdeadbeef") + assert(convert(testNativeError) == "lookup failed") + for _, value in ipairs({{}, function() end, convert, testUserdata, testThread, testChannel, testGoStruct, testIPs}) do local ok, result = pcall(convert, value) assert(ok and result == "") end @@ -642,6 +649,21 @@ string.__tostring = nil `) var metamethodCalls int L, err := program.NewState(context.Background(), func(L *glua.LState) { + L.SetGlobal("testIPv4", luar.New(L, net.ParseIP("192.0.2.1"))) + L.SetGlobal("testIPv6", luar.New(L, net.ParseIP("2001:db8::1"))) + PushUserData(L, net.ParseIP("198.51.100.1")) + L.SetGlobal("testNativeIP", L.Get(-1)) + L.Pop(1) + PushUserData(L, bytes.NewBufferString("0xdeadbeef")) + L.SetGlobal("testNativeStringer", L.Get(-1)) + L.Pop(1) + PushError(L, errors.New("lookup failed")) + L.SetGlobal("testNativeError", L.Get(-1)) + L.Pop(1) + L.SetGlobal("testGoStruct", luar.New(L, &struct{ Value string }{"readable field"})) + NewSlicePusher[net.IP](L)(L, []net.IP{net.ParseIP("192.0.2.1")}) + L.SetGlobal("testIPs", L.Get(-1)) + L.Pop(1) userdata := L.NewUserData() metatable := L.NewTable() metatable.RawSetString("__tostring", L.NewFunction(func(L *glua.LState) int {