Files
Xray-core/common/lua/program_test.go
T
2026-10-14 07:07:03 +08:00

838 lines
31 KiB
Go

package lua
import (
"bytes"
"compress/gzip"
"context"
"crypto/ed25519"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"errors"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"testing"
"time"
"weak"
xbase64 "github.com/xtls/xray-core/common/base64"
glua "github.com/yuin/gopher-lua"
)
func TestProgramStatesAreIndependent(t *testing.T) {
path := filepath.Join(t.TempDir(), "state.lua")
if err := os.WriteFile(path, []byte("value = (value or 0) + 1"), 0o600); err != nil {
t.Fatal(err)
}
program, err := Compile(path)
if err != nil {
t.Fatal(err)
}
if program.proto.SourceName != path {
t.Fatalf("program source = %q, want %q", program.proto.SourceName, path)
}
first, err := program.NewState(context.Background(), nil, nil)
if err != nil {
t.Fatal(err)
}
defer first.Close()
first.SetGlobal("value", glua.LNumber(42))
second, err := program.NewState(context.Background(), nil, nil)
if err != nil {
t.Fatal(err)
}
defer second.Close()
if got := second.GetGlobal("value"); got != glua.LNumber(1) {
t.Fatalf("second state value = %v, want 1", got)
}
}
func TestProgramInitializationObservesCancellation(t *testing.T) {
path := filepath.Join(t.TempDir(), "loop.lua")
if err := os.WriteFile(path, []byte("while true do end"), 0o600); err != nil {
t.Fatal(err)
}
program, err := Compile(path)
if err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
state, err := program.NewState(ctx, nil, nil)
if err == nil || state != nil {
if state != nil {
state.Close()
}
t.Fatalf("NewState with canceled context = %v, %v; want nil state and error", state, err)
}
}
func TestNewStateClosesFailedValidation(t *testing.T) {
path := filepath.Join(t.TempDir(), "state.lua")
if err := os.WriteFile(path, []byte("value = 1"), 0o600); err != nil {
t.Fatal(err)
}
program, err := Compile(path)
if err != nil {
t.Fatal(err)
}
wantErr := errors.New("invalid script")
var checked *glua.LState
L, err := program.NewState(context.Background(), nil, func(L *glua.LState) error {
checked = L
return wantErr
})
if L != nil || !errors.Is(err, wantErr) || checked == nil || !checked.IsClosed() {
t.Fatalf("state = %v, error = %v, checked state closed = %t", L, err, checked != nil && checked.IsClosed())
}
}
func compressSource(t *testing.T, code string) []byte {
t.Helper()
var data bytes.Buffer
writer := gzip.NewWriter(&data)
if _, err := writer.Write([]byte(code)); err != nil {
t.Fatal(err)
}
if err := writer.Close(); err != nil {
t.Fatal(err)
}
return data.Bytes()
}
func TestCompileInline(t *testing.T) {
const code = `value = "你好"`
for _, tc := range []struct {
name, script string
value glua.LValue
}{
{"base64", xbase64.Prefix + base64.StdEncoding.EncodeToString([]byte(code)), glua.LString("你好")},
{"base64+gzip", xbase64.GzipPrefix + base64.StdEncoding.EncodeToString(compressSource(t, code)), glua.LString("你好")},
{"empty base64", xbase64.Prefix, glua.LNil},
} {
t.Run(tc.name, func(t *testing.T) {
program, err := Compile(tc.script)
if err != nil {
t.Fatal(err)
}
if program.proto.SourceName != "[inline]" {
t.Fatalf("program source = %q, want [inline]", program.proto.SourceName)
}
state, err := program.NewState(context.Background(), nil, nil)
if err != nil {
t.Fatal(err)
}
defer state.Close()
if got := state.GetGlobal("value"); got != tc.value {
t.Fatalf("value = %v, want %v", got, tc.value)
}
})
}
}
func TestCompileRejectsInvalidInline(t *testing.T) {
corrupt := compressSource(t, "value = 1")
corrupt[len(corrupt)-8] ^= 0xff
for _, tc := range []struct {
name, script string
wantErr error
wantMessage string
}{
{"base64", xbase64.Prefix + "%", base64.CorruptInputError(0), "failed to decode base64"},
{"gzip base64", xbase64.GzipPrefix + "%", base64.CorruptInputError(0), "failed to decode base64"},
{"gzip header", xbase64.GzipPrefix + base64.StdEncoding.EncodeToString([]byte("value = 'uncompressed'")), gzip.ErrHeader, "failed to decompress gzip"},
{"gzip checksum", xbase64.GzipPrefix + base64.StdEncoding.EncodeToString(corrupt), gzip.ErrChecksum, "failed to decompress gzip"},
} {
t.Run(tc.name, func(t *testing.T) {
program, err := Compile(tc.script)
if program != nil || !errors.Is(err, tc.wantErr) || !strings.Contains(err.Error(), tc.wantMessage) {
t.Fatalf("Compile = %v, %v; want nil program and %q error wrapping %v", program, err, tc.wantMessage, tc.wantErr)
}
})
}
}
func TestCompileInlineDiagnostics(t *testing.T) {
for _, tc := range []struct {
code string
compileError bool
wantMessage string
}{
{"function broken(", true, ""},
{`error("initialization failed")`, false, ""},
{"\n-- @xray:privileged-signature=invalid\n\nerror('failure')", false, "[inline]:4: failure"},
} {
program, err := Compile(xbase64.Prefix + base64.StdEncoding.EncodeToString([]byte(tc.code)))
if tc.compileError {
if program != nil || err == nil {
t.Fatalf("Compile = %v, %v; want nil program and syntax error", program, err)
}
} else {
if err != nil {
t.Fatal(err)
}
state, initErr := program.NewState(context.Background(), nil, nil)
if state != nil {
state.Close()
t.Fatal("NewState returned a state after failed initialization")
}
err = initErr
}
if err == nil || !strings.Contains(err.Error(), "[inline]") {
t.Fatalf("Lua error = %v, want [inline] diagnostic", err)
}
if tc.wantMessage != "" && !strings.Contains(err.Error(), tc.wantMessage) {
t.Fatalf("Lua error = %v, want %q", err, tc.wantMessage)
}
}
}
func TestCompileRejectsReadFailure(t *testing.T) {
path := filepath.Join(t.TempDir(), "unreadable.lua")
if err := os.WriteFile(path, []byte("value = 42"), 0o600); err != nil {
t.Fatal(err)
}
if err := os.Chmod(path, 0); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { os.Chmod(path, 0o600) })
if _, err := os.ReadFile(path); !errors.Is(err, os.ErrPermission) {
t.Skip("file read permissions are not enforced")
}
if program, err := Compile(path); program != nil || !errors.Is(err, os.ErrPermission) {
t.Fatalf("Compile = %v, %v; want nil program and permission error", program, err)
}
}
func compileTestProgram(t *testing.T, code string) *Program {
t.Helper()
program, err := Compile(xbase64.Prefix + base64.StdEncoding.EncodeToString([]byte(code)))
if err != nil {
t.Fatal(err)
}
return program
}
const privilegedSignatureHeader = "-- @xray:privileged-signature="
// RFC 8032 test-vector key. The fixture signature was independently computed
// with Node's OpenSSL-backed crypto.sign(null, SHA256(body), privateKey), rather
// than by the signing helper or the production verifier.
const (
privilegedTestSeed = "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60"
privilegedTestPublicKey = "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"
privilegedFixtureSignature = "rFbZtaaphe_69UEgSvfYwZcLZYHTXLFxdcukfEuLq6pzvmj8qtw6r60fD-7L5dwqSvnE7F1oAV0G_vHP3rLVBA"
privilegedFixtureDigest = "edef639ae641bacc9b76cea27473eed11369fbad65e223a87990864e899e578a"
privilegedFixtureBody = `-- Ordinary comments participate in the signed digest.
value = 1
function HasPrivilegedLibraries()
return io ~= nil and os ~= nil and debug ~= nil and channel ~= nil
end`
)
func setTestPrivilegedPublicKey(t *testing.T, key string) {
t.Helper()
// Each test setup represents a new process with its own startup key.
privilegedKey.Once = sync.Once{}
privilegedKey.publicKey = nil
t.Cleanup(func() {
privilegedKey.Once = sync.Once{}
privilegedKey.publicKey = nil
})
if err := SetPrivilegedPublicKey(key); err != nil {
t.Fatal(err)
}
}
func writeTestScript(t *testing.T, code string) string {
t.Helper()
path := filepath.Join(t.TempDir(), "script.lua")
if err := os.WriteFile(path, []byte(code), 0o600); err != nil {
t.Fatal(err)
}
return path
}
func testSignature(t *testing.T, body string) string {
t.Helper()
seed, err := hex.DecodeString(privilegedTestSeed)
if err != nil {
t.Fatal(err)
}
digest := sha256.Sum256(bytes.TrimSpace([]byte(body)))
return base64.RawURLEncoding.EncodeToString(ed25519.Sign(ed25519.NewKeyFromSeed(seed), digest[:]))
}
func TestSetPrivilegedPublicKey(t *testing.T) {
setTestPrivilegedPublicKey(t, privilegedTestPublicKey)
path := writeTestScript(t, privilegedSignatureHeader+privilegedFixtureSignature+"\n"+privilegedFixtureBody)
for _, tc := range []struct{ name, key string }{
{"invalid base64", "%"},
{"padding", privilegedTestPublicKey + "="},
{"standard base64 alphabet", strings.ReplaceAll(privilegedTestPublicKey, "_", "/")},
{"short key", base64.RawURLEncoding.EncodeToString(make([]byte, ed25519.PublicKeySize-1))},
{"long key", base64.RawURLEncoding.EncodeToString(make([]byte, ed25519.PublicKeySize+1))},
{"leading whitespace", " " + privilegedTestPublicKey},
{"trailing whitespace", privilegedTestPublicKey + "\t"},
{"embedded newline", privilegedTestPublicKey[:8] + "\n" + privilegedTestPublicKey[8:]},
{"embedded carriage return", privilegedTestPublicKey[:8] + "\r" + privilegedTestPublicKey[8:]},
{"nonzero unused bits", privilegedTestPublicKey[:len(privilegedTestPublicKey)-1] + "p"},
} {
t.Run(tc.name, func(t *testing.T) {
if err := SetPrivilegedPublicKey(tc.key); err == nil {
t.Fatal("invalid public key was accepted")
}
program, err := Compile(path)
if err != nil || program == nil || !program.privileged {
t.Fatalf("invalid setter changed the trusted key: Compile = %v, %v", program, err)
}
})
}
setTestPrivilegedPublicKey(t, "")
if program, err := Compile(path); program != nil || err == nil {
t.Fatalf("signed script after disabling authorization = %v, %v; want rejection", program, err)
}
unsigned, err := Compile(writeTestScript(t, privilegedFixtureBody))
if err != nil || unsigned == nil || unsigned.privileged {
t.Fatalf("unsigned script with authorization disabled = %v, %v; want sandbox", unsigned, err)
}
}
func TestPrivilegedPublicKeyIsFixed(t *testing.T) {
wrongKey := base64.RawURLEncoding.EncodeToString(make([]byte, ed25519.PublicKeySize))
for _, key := range []string{"", privilegedTestPublicKey} {
name := "disabled"
if key != "" {
name = "configured"
}
t.Run(name, func(t *testing.T) {
setTestPrivilegedPublicKey(t, key)
path := writeTestScript(t, privilegedSignatureHeader+privilegedFixtureSignature+"\n"+privilegedFixtureBody)
for _, replacement := range []string{privilegedTestPublicKey, "", wrongKey} {
if err := SetPrivilegedPublicKey(replacement); err != nil {
t.Fatal(err)
}
program, err := Compile(path)
if key == "" {
if program != nil || err == nil {
t.Fatalf("disabled startup key changed: Compile = %v, %v", program, err)
}
} else if err != nil || program == nil || !program.privileged {
t.Fatalf("trusted startup key changed: Compile = %v, %v", program, err)
}
}
})
}
}
func TestProgramPrivilegedSignatureFixture(t *testing.T) {
setTestPrivilegedPublicKey(t, privilegedTestPublicKey)
digest := sha256.Sum256([]byte(privilegedFixtureBody))
if got := hex.EncodeToString(digest[:]); got != privilegedFixtureDigest {
t.Fatalf("fixture SHA-256 = %s, want %s", got, privilegedFixtureDigest)
}
for _, signed := range []bool{false, true} {
name, code := "unsigned", privilegedFixtureBody
if signed {
name, code = "signed", privilegedSignatureHeader+privilegedFixtureSignature+"\n"+code
}
t.Run(name, func(t *testing.T) {
program, err := Compile(writeTestScript(t, code))
if err != nil {
t.Fatal(err)
}
if program.privileged != signed {
t.Fatalf("privileged = %t, want %t", program.privileged, signed)
}
L, err := program.NewState(context.Background(), nil, nil)
if err != nil {
t.Fatal(err)
}
defer L.Close()
if err := L.CallByParam(glua.P{Fn: L.GetGlobal("HasPrivilegedLibraries"), NRet: 1, Protect: true}); err != nil {
t.Fatal(err)
}
if got := L.Get(-1); got != glua.LBool(signed) {
t.Fatalf("privileged libraries available = %v, want %t", got, signed)
}
})
}
}
func TestProgramPrivilegedSignatureHeader(t *testing.T) {
setTestPrivilegedPublicKey(t, privilegedTestPublicKey)
header := privilegedSignatureHeader + privilegedFixtureSignature
for _, tc := range []struct {
name, code string
privileged bool
}{
{"match", header + "\n" + privilegedFixtureBody, true},
{"no comment space", "--@xray:privileged-signature=" + privilegedFixtureSignature + "\n" + privilegedFixtureBody, true},
{"comment whitespace", "-- \t @xray:privileged-signature=" + privilegedFixtureSignature + "\n" + privilegedFixtureBody, true},
{"signature whitespace", privilegedSignatureHeader + " \t" + privilegedFixtureSignature + " \t\n" + privilegedFixtureBody, true},
{"blank lines and indentation", " \t\r\n\n \t" + header + "\n" + privilegedFixtureBody, true},
{"CRLF header only", "\r\n" + header + "\r\n" + privilegedFixtureBody, true},
{"body boundary whitespace", header + "\n \t\r\n\n" + privilegedFixtureBody + " \t\r\n\n", true},
{"missing header", privilegedFixtureBody, false},
{"after comment", "-- ordinary comment\n" + header + "\n" + privilegedFixtureBody, false},
{"after code", "value = 1\n" + header + "\n" + privilegedFixtureBody, false},
{"inside string", `value = "` + header + `"`, false},
{"wrong marker", "-- xray:privileged-signature=" + privilegedFixtureSignature + "\n" + privilegedFixtureBody, false},
{"old PSK first", "-- @xray:privileged-psk=secret\n" + header + "\n" + privilegedFixtureBody, false},
{"invalid marker after comment", "-- ordinary comment\n" + privilegedSignatureHeader + "%\nvalue = 1", false},
} {
t.Run(tc.name, func(t *testing.T) {
program, err := Compile(writeTestScript(t, tc.code))
if err != nil {
t.Fatal(err)
}
if program.privileged != tc.privileged {
t.Fatalf("privileged = %t, want %t", program.privileged, tc.privileged)
}
})
}
}
func TestProgramRejectsInvalidPrivilegedSignatures(t *testing.T) {
seed, err := hex.DecodeString(privilegedTestSeed)
if err != nil {
t.Fatal(err)
}
wrongKey := base64.RawURLEncoding.EncodeToString(ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize)).Public().(ed25519.PublicKey))
bodySignature := base64.RawURLEncoding.EncodeToString(ed25519.Sign(ed25519.NewKeyFromSeed(seed), []byte(privilegedFixtureBody)))
for _, tc := range []struct{ name, key, signature string }{
{"missing key", "", privilegedFixtureSignature},
{"wrong key", wrongKey, privilegedFixtureSignature},
{"empty signature", privilegedTestPublicKey, ""},
{"whitespace-only signature", privilegedTestPublicKey, " \t"},
{"invalid base64", privilegedTestPublicKey, "%"},
{"padding", privilegedTestPublicKey, privilegedFixtureSignature + "=="},
{"standard base64 alphabet", privilegedTestPublicKey, strings.ReplaceAll(privilegedFixtureSignature, "_", "/")},
{"short signature", privilegedTestPublicKey, base64.RawURLEncoding.EncodeToString(make([]byte, ed25519.SignatureSize-1))},
{"long signature", privilegedTestPublicKey, base64.RawURLEncoding.EncodeToString(make([]byte, ed25519.SignatureSize+1))},
{"embedded carriage return", privilegedTestPublicKey, privilegedFixtureSignature[:8] + "\r" + privilegedFixtureSignature[8:]},
{"nonzero unused bits", privilegedTestPublicKey, privilegedFixtureSignature[:len(privilegedFixtureSignature)-1] + "B"},
{"damaged signature", privilegedTestPublicKey, "s" + privilegedFixtureSignature[1:]},
{"signature over body instead of digest", privilegedTestPublicKey, bodySignature},
} {
t.Run(tc.name, func(t *testing.T) {
setTestPrivilegedPublicKey(t, tc.key)
program, err := Compile(writeTestScript(t, privilegedSignatureHeader+tc.signature+"\n"+privilegedFixtureBody))
if program != nil || err == nil {
t.Fatalf("Compile = %v, %v; want nil program and signature rejection", program, err)
}
})
}
setTestPrivilegedPublicKey(t, privilegedTestPublicKey)
code := privilegedSignatureHeader + "%\n" + privilegedSignatureHeader + privilegedFixtureSignature + "\n" + privilegedFixtureBody
if program, err := Compile(writeTestScript(t, code)); program != nil || err == nil {
t.Fatalf("later signature replaced invalid first signature: %v, %v", program, err)
}
}
func TestProgramSignedBodyTampering(t *testing.T) {
setTestPrivilegedPublicKey(t, privilegedTestPublicKey)
for _, tc := range []struct{ name, body string }{
{"statement", strings.Replace(privilegedFixtureBody, "value = 1", "value = 2", 1)},
{"comment", strings.Replace(privilegedFixtureBody, "Ordinary", "Modified", 1)},
{"added comment", "-- added comment\n" + privilegedFixtureBody},
{"indentation", strings.Replace(privilegedFixtureBody, " return", " return", 1)},
{"internal spaces", strings.Replace(privilegedFixtureBody, "value = 1", "value = 1", 1)},
{"internal trailing spaces", strings.Replace(privilegedFixtureBody, "value = 1\n", "value = 1 \t\n", 1)},
{"removed blank line", strings.Replace(privilegedFixtureBody, "\n\n", "\n", 1)},
{"added blank line", strings.Replace(privilegedFixtureBody, "\n\n", "\n\n\n", 1)},
{"LF to CRLF", strings.ReplaceAll(privilegedFixtureBody, "\n", "\r\n")},
} {
t.Run(tc.name, func(t *testing.T) {
code := privilegedSignatureHeader + privilegedFixtureSignature + "\n" + tc.body
if program, err := Compile(writeTestScript(t, code)); program != nil || err == nil {
t.Fatalf("tampered body Compile = %v, %v; want signature rejection", program, err)
}
})
}
crlfBody := strings.ReplaceAll(privilegedFixtureBody, "\n", "\r\n")
crlfSignature := testSignature(t, crlfBody)
program, err := Compile(writeTestScript(t, privilegedSignatureHeader+crlfSignature+"\r\n"+crlfBody))
if err != nil || program == nil || !program.privileged {
t.Fatalf("correctly signed CRLF body = %v, %v; want privileged program", program, err)
}
if program, err := Compile(writeTestScript(t, privilegedSignatureHeader+crlfSignature+"\n"+privilegedFixtureBody)); program != nil || err == nil {
t.Fatalf("CRLF to LF tampering = %v, %v; want signature rejection", program, err)
}
}
func TestProgramInlineSignaturesCannotAuthorize(t *testing.T) {
for _, key := range []string{"", privilegedTestPublicKey} {
setTestPrivilegedPublicKey(t, key)
for _, signature := range []string{privilegedFixtureSignature, "%", ""} {
code := privilegedSignatureHeader + signature + "\n" + privilegedFixtureBody
for _, script := range []string{
xbase64.Prefix + base64.StdEncoding.EncodeToString([]byte(code)),
xbase64.GzipPrefix + base64.StdEncoding.EncodeToString(compressSource(t, code)),
} {
program, err := Compile(script)
if err != nil || program == nil || program.privileged {
t.Fatalf("inline signed source Compile = %v, %v; want sandbox without verification", program, err)
}
L, err := program.NewState(context.Background(), nil, nil)
if err != nil {
t.Fatal(err)
}
if got := L.GetGlobal("os"); got != glua.LNil {
L.Close()
t.Fatalf("inline os = %v, want nil", got)
}
L.Close()
}
}
}
}
func TestProgramLegacyPSKCannotAuthorize(t *testing.T) {
setTestPrivilegedPublicKey(t, privilegedTestPublicKey)
t.Setenv("xray.lua.privileged.psk", "secret")
t.Setenv("XRAY_LUA_PRIVILEGED_PSK", "secret")
program, err := Compile(writeTestScript(t, "-- @xray:privileged-psk=secret\n"+privilegedFixtureBody))
if err != nil || program == nil || program.privileged {
t.Fatalf("old PSK authorization = %v, %v; want sandbox", program, err)
}
L, err := program.NewState(context.Background(), nil, nil)
if err != nil {
t.Fatal(err)
}
defer L.Close()
if got := L.GetGlobal("os"); got != glua.LNil {
t.Fatalf("old PSK os = %v, want nil", got)
}
setTestPrivilegedPublicKey(t, "")
if program, err := Compile(writeTestScript(t, privilegedSignatureHeader+privilegedFixtureSignature+"\n"+privilegedFixtureBody)); program != nil || err == nil {
t.Fatalf("legacy environment substituted for public key: %v, %v", program, err)
}
}
func TestProgramSignedDiagnostics(t *testing.T) {
setTestPrivilegedPublicKey(t, privilegedTestPublicKey)
const invalidBody = "\n\nlocal = 1"
invalidPath := writeTestScript(t, "\n"+privilegedSignatureHeader+"%\n"+invalidBody)
if program, err := Compile(invalidPath); program != nil || err == nil || !strings.Contains(err.Error(), "signature") {
t.Fatalf("invalid signature and invalid syntax = %v, %v; want signature error before parsing", program, err)
}
validPath := writeTestScript(t, "\n"+privilegedSignatureHeader+testSignature(t, invalidBody)+"\n"+invalidBody)
if program, err := Compile(validPath); program != nil || err == nil || !strings.Contains(err.Error(), validPath+" line:5(") {
t.Fatalf("verified invalid syntax = %v, %v; want original source line 5", program, err)
}
const body = "\n\nerror('signed failure')"
path := writeTestScript(t, "\n"+privilegedSignatureHeader+testSignature(t, body)+"\n"+body)
program, err := Compile(path)
if err != nil {
t.Fatal(err)
}
L, err := program.NewState(context.Background(), nil, nil)
if L != nil {
L.Close()
t.Fatal("failing signed script returned a state")
}
if err == nil || !strings.Contains(err.Error(), path+":5: signed failure") {
t.Fatalf("signed initialization error = %v; want original source line 5", err)
}
}
func TestProgramSandbox(t *testing.T) {
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "escape.lua"), []byte(`disk_loaded = true; return "escaped"`), 0o600); err != nil {
t.Fatal(err)
}
t.Setenv("LUA_PATH", filepath.ToSlash(filepath.Join(dir, "?.lua")))
program := compileTestProgram(t, `
local function check()
assert(package == nil)
assert(require("_G") == _G)
for _, name in ipairs({"io", "os", "debug", "channel", "dofile", "loadfile",
"load", "loadstring", "getfenv", "setfenv", "collectgarbage", "module",
"newproxy", "print", "_printregs"}) do
assert(_G[name] == nil, name)
end
for _, name in ipairs({"io", "os", "debug", "channel", "package", "escape"}) do
assert(not pcall(require, name), name)
end
for _, name in ipairs({"math", "string", "table", "coroutine"}) do
assert(require(name) == _G[name], name)
end
local helper = require("xray.test")
assert(helper.value == "ok" and helper == require("xray.test"))
assert(math.abs(-2) == 2 and string.upper("ok") == "OK")
assert(table.concat({"a", "b"}) == "ab")
end
check()
function CheckSandbox()
local thread = coroutine.create(check)
assert(coroutine.resume(thread))
local injected = false
package = {
preload = {
escape = function() injected = true; return "escaped" end,
["xray.late"] = function() injected = true; return "injected" end,
},
loaded = {escape = "escaped", ["cached.only"] = "injected"},
loaders = {function() injected = true; return function() return "escaped" end end},
path = testPath,
cpath = testPath,
}
assert(not pcall(require, "escape") and disk_loaded == nil)
assert(not pcall(require, "cached.only") and not pcall(require, "package"))
assert(require("xray.late") == "host" and require("xray.late") == "host")
assert(require("xray.test").value == "ok" and not injected)
package = nil
end`)
var helperLoads, lateLoads int
L, err := program.NewState(context.Background(), func(L *glua.LState) {
if L.GetGlobal("io") != glua.LNil || L.GetGlobal("loadfile") != glua.LNil {
t.Fatal("sandbox was not configured before module registration")
}
L.SetGlobal("testPath", glua.LString(filepath.ToSlash(filepath.Join(dir, "?.lua"))))
L.PreloadModule("xray.test", func(L *glua.LState) int {
helperLoads++
module := L.NewTable()
module.RawSetString("value", glua.LString("ok"))
L.Push(module)
return 1
})
L.PreloadModule("xray.late", func(L *glua.LState) int {
lateLoads++
L.Push(glua.LString("host"))
return 1
})
}, nil)
if err != nil {
t.Fatal(err)
}
defer L.Close()
if L.GetTop() != 0 {
t.Fatalf("stack top = %d, want 0", L.GetTop())
}
if err := L.CallByParam(glua.P{Fn: L.GetGlobal("CheckSandbox"), Protect: true}); err != nil {
t.Fatal(err)
}
if helperLoads != 1 || lateLoads != 1 {
t.Fatalf("module initialization calls = (%d, %d), want (1, 1)", helperLoads, lateLoads)
}
}
func TestProgramSandboxModuleFailures(t *testing.T) {
program := compileTestProgram(t, `
assert(package == nil)
local ok, err = pcall(require, "xray.failure")
assert(not ok and string.find(err, "module initialization failed", 1, true))
package = {loaded = {["xray.failure"] = "injected"}}
ok, err = pcall(require, "xray.failure")
assert(not ok and string.find(err, "loop or previous error loading module", 1, true))
for i = 1, 2 do
ok, err = pcall(require, "xray.cycle")
assert(not ok and string.find(err, "loop or previous error loading module", 1, true))
end
package = nil
`)
var failedLoads, cycleLoads int
L, err := program.NewState(context.Background(), func(L *glua.LState) {
L.PreloadModule("xray.failure", func(L *glua.LState) int {
failedLoads++
L.RaiseError("module initialization failed")
return 0
})
L.PreloadModule("xray.cycle", func(L *glua.LState) int {
cycleLoads++
L.Push(L.GetGlobal("require"))
L.Push(glua.LString("xray.cycle"))
L.Call(1, 1)
return 1
})
}, nil)
if err != nil {
t.Fatal(err)
}
defer L.Close()
if failedLoads != 1 || cycleLoads != 1 {
t.Fatalf("module initialization calls = (%d, %d), want (1, 1)", failedLoads, cycleLoads)
}
if L.GetTop() != 0 {
t.Fatalf("stack top = %d, want 0", L.GetTop())
}
}
func TestProgramSandboxModulesAreCollected(t *testing.T) {
for _, fail := range []bool{false, true} {
name := "success"
if fail {
name = "failure"
}
t.Run(name, func(t *testing.T) {
ref := func() weak.Pointer[glua.LTable] {
program := compileTestProgram(t, `payload = string.rep("x", 1024 * 1024); assert(pcall(require, "xray.test") == expectedSuccess)`)
L, err := program.NewState(context.Background(), func(L *glua.LState) {
L.SetGlobal("expectedSuccess", glua.LBool(!fail))
L.PreloadModule("xray.test", func(L *glua.LState) int {
if fail {
L.RaiseError("module initialization failed")
}
L.Push(L.NewTable())
return 1
})
}, nil)
if err != nil {
t.Fatal(err)
}
ref := weak.Make(L.G.Global)
L.Close()
return ref
}()
runtime.GC()
runtime.GC()
if ref.Value() != nil {
t.Fatal("closed sandbox's global table was not collected")
}
})
}
}
func TestProgramSandboxClosesFailedInitialization(t *testing.T) {
program := compileTestProgram(t, `require("xray.failure")`)
var partial *glua.LState
L, err := program.NewState(context.Background(), func(L *glua.LState) {
partial = L
L.PreloadModule("xray.failure", func(L *glua.LState) int {
L.RaiseError("module initialization failed")
return 0
})
}, func(*glua.LState) error {
t.Error("validation ran after failed initialization")
return nil
})
if L != nil {
L.Close()
t.Fatal("sandbox initialization returned a state")
}
if err == nil || !strings.Contains(err.Error(), "module initialization failed") || partial == nil || !partial.IsClosed() {
t.Fatalf("error = %v, partial state closed = %t", err, partial != nil && partial.IsClosed())
}
}
func TestProgramPrivilegedLibraries(t *testing.T) {
setTestPrivilegedPublicKey(t, privilegedTestPublicKey)
const body = `assert(require("io") == io and require("os") == os)
assert(require("debug") == debug and require("channel") == channel)
assert(type(coroutine.create) == "function" and require("coroutine") == coroutine)
assert(type(dofile) == "function" and type(loadfile) == "function")
assert(type(getfenv) == "function" and type(setfenv) == "function")
assert(type(collectgarbage) == "function" and type(print) == "function")
assert(type(package.loaders[2]) == "function" and type(package.path) == "string")
assert(type(debug.traceback()) == "string" and type(os.time()) == "number")
assert(assert(loadstring("return 42"))() == 42)
local file = assert(io.open(testPath, "w"))
file:write("privileged")
file:close()`
code := privilegedSignatureHeader + testSignature(t, body) + "\n" + body
path := filepath.Join(t.TempDir(), "privileged.lua")
if err := os.WriteFile(path, []byte(code), 0o600); err != nil {
t.Fatal(err)
}
for _, tc := range []struct {
name, script string
privileged bool
}{
{"file", path, true},
{"base64", xbase64.Prefix + base64.StdEncoding.EncodeToString([]byte(code)), false},
{"gzip", xbase64.GzipPrefix + base64.StdEncoding.EncodeToString(compressSource(t, code)), false},
} {
t.Run(tc.name, func(t *testing.T) {
program, err := Compile(tc.script)
if err != nil {
t.Fatal(err)
}
output := filepath.Join(t.TempDir(), "privileged.txt")
L, err := program.NewState(context.Background(), func(L *glua.LState) {
L.SetGlobal("testPath", glua.LString(output))
}, nil)
if !tc.privileged {
if L != nil {
L.Close()
t.Fatal("inline script with a valid signature accessed privileged libraries")
}
if err == nil || !strings.Contains(err.Error(), "no preloaded module: io") {
t.Fatalf("NewState error = %v, want io module unavailable", err)
}
if _, err := os.Stat(output); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("inline script output stat error = %v, want file not found", err)
}
return
}
if err != nil {
t.Fatal(err)
}
defer L.Close()
data, err := os.ReadFile(output)
if err != nil || string(data) != "privileged" {
t.Fatalf("privileged file contents = %q, error = %v", data, err)
}
})
}
}
func TestProgramPrivilegesAreCompileTimeSnapshot(t *testing.T) {
setTestPrivilegedPublicKey(t, privilegedTestPublicKey)
for _, privileged := range []bool{false, true} {
name := "sandboxed"
if privileged {
name = "privileged"
}
t.Run(name, func(t *testing.T) {
const body = "value = 1"
code := body
if privileged {
code = privilegedSignatureHeader + testSignature(t, body) + "\n" + body
}
path := filepath.Join(t.TempDir(), "snapshot.lua")
if err := os.WriteFile(path, []byte(code), 0o600); err != nil {
t.Fatal(err)
}
program, err := Compile(path)
if err != nil {
t.Fatal(err)
}
// Source edits cannot change an already compiled Program's authorization.
changed := privilegedSignatureHeader + testSignature(t, body) + "\n" + body
if privileged {
changed = body
}
if err := os.WriteFile(path, []byte(changed), 0o600); err != nil {
t.Fatal(err)
}
check := func(L *glua.LState) {
t.Helper()
if got := L.GetGlobal("os").Type() == glua.LTTable; got != privileged {
t.Fatalf("os available = %t, want compile-time value %t", got, privileged)
}
}
factory := program.NewStateFactory(time.Second, nil, nil)
for range 2 {
L, err := factory(context.Background())
if err != nil {
t.Fatal(err)
}
defer L.Close()
check(L)
}
recompiled, err := Compile(path)
if err != nil {
t.Fatal(err)
}
if recompiled.privileged == privileged {
t.Fatal("recompilation did not observe the changed source")
}
})
}
}