new plugin system
This commit is contained in:
@@ -486,7 +486,7 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
|
||||
return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: false, Message: "missing payload"})
|
||||
}
|
||||
manifestRaw, _ := payload["manifest"].(map[string]interface{})
|
||||
wasmBytes, _ := payload["wasm"].([]byte)
|
||||
binaryBytes, _ := payload["binary"].([]byte)
|
||||
if env.Plugins == nil {
|
||||
return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: false, Message: "plugin manager not ready"})
|
||||
}
|
||||
@@ -494,7 +494,7 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
|
||||
if err != nil {
|
||||
return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: false, Message: err.Error()})
|
||||
}
|
||||
if err := env.Plugins.Load(ctx, manifest, wasmBytes); err != nil {
|
||||
if err := env.Plugins.Load(ctx, manifest, binaryBytes); err != nil {
|
||||
_ = wire.WriteMsg(ctx, env.Conn, wire.PluginEvent{Type: "plugin_event", PluginID: manifest.ID, Event: "error", Error: err.Error()})
|
||||
return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: false, Message: err.Error()})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
//go:build darwin && cgo
|
||||
|
||||
package plugins
|
||||
|
||||
/*
|
||||
#include <dlfcn.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
static void* so_dlopen(const char* path, char** errOut) {
|
||||
void* h = dlopen(path, RTLD_NOW | RTLD_LOCAL);
|
||||
if (!h && errOut) *errOut = (char*)dlerror();
|
||||
return h;
|
||||
}
|
||||
|
||||
static void* so_dlsym(void* h, const char* name, char** errOut) {
|
||||
void* s = dlsym(h, name);
|
||||
if (!s && errOut) *errOut = (char*)dlerror();
|
||||
return s;
|
||||
}
|
||||
|
||||
static int so_dlclose(void* h) {
|
||||
if (h) return dlclose(h);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Go callback bridge — forward-declared here, implemented via //export below.
|
||||
extern void goPluginHostCallback(
|
||||
uintptr_t ctx,
|
||||
const char* event, int eventLen,
|
||||
const char* payload, int payloadLen);
|
||||
|
||||
// Call the plugin's PluginOnLoad, injecting our callback bridge + context.
|
||||
static int so_call_onload(void* fn, const char* info, int infoLen, uintptr_t ctx) {
|
||||
typedef int (*fn_t)(const char*, int, uintptr_t, uintptr_t);
|
||||
return ((fn_t)fn)(info, infoLen, (uintptr_t)goPluginHostCallback, ctx);
|
||||
}
|
||||
|
||||
static int so_call_onevent(void* fn, const char* ev, int evLen, const char* pl, int plLen) {
|
||||
typedef int (*fn_t)(const char*, int, const char*, int);
|
||||
return ((fn_t)fn)(ev, evLen, pl, plLen);
|
||||
}
|
||||
|
||||
static void so_call_onunload(void* fn) {
|
||||
typedef void (*fn_t)(void);
|
||||
((fn_t)fn)();
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime/cgo"
|
||||
"sync"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
//export goPluginHostCallback
|
||||
func goPluginHostCallback(ctx C.uintptr_t, event *C.char, eventLen C.int, payload *C.char, payloadLen C.int) {
|
||||
h := cgo.Handle(ctx)
|
||||
send := h.Value().(func(string, []byte))
|
||||
ev := C.GoStringN(event, eventLen)
|
||||
var pl []byte
|
||||
if payloadLen > 0 {
|
||||
pl = C.GoBytes(unsafe.Pointer(payload), payloadLen)
|
||||
}
|
||||
send(ev, pl)
|
||||
}
|
||||
|
||||
func loadNativePlugin(data []byte) (NativePlugin, error) {
|
||||
if len(data) == 0 {
|
||||
return nil, errors.New("empty plugin binary")
|
||||
}
|
||||
|
||||
f, err := os.CreateTemp("", "*.dylib")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create temp: %w", err)
|
||||
}
|
||||
tmpPath := f.Name()
|
||||
|
||||
if _, err := f.Write(data); err != nil {
|
||||
f.Close()
|
||||
os.Remove(tmpPath)
|
||||
return nil, fmt.Errorf("write temp: %w", err)
|
||||
}
|
||||
f.Close()
|
||||
|
||||
cPath := C.CString(tmpPath)
|
||||
defer C.free(unsafe.Pointer(cPath))
|
||||
var cErr *C.char
|
||||
handle := C.so_dlopen(cPath, &cErr)
|
||||
|
||||
os.Remove(tmpPath)
|
||||
|
||||
if handle == nil {
|
||||
return nil, fmt.Errorf("dlopen: %s", C.GoString(cErr))
|
||||
}
|
||||
|
||||
resolve := func(name string) (unsafe.Pointer, error) {
|
||||
cName := C.CString(name)
|
||||
defer C.free(unsafe.Pointer(cName))
|
||||
var cErr *C.char
|
||||
s := C.so_dlsym(handle, cName, &cErr)
|
||||
if s == nil {
|
||||
return nil, fmt.Errorf("dlsym(%s): %s", name, C.GoString(cErr))
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
onLoad, err := resolve("PluginOnLoad")
|
||||
if err != nil {
|
||||
C.so_dlclose(handle)
|
||||
return nil, err
|
||||
}
|
||||
onEvent, err := resolve("PluginOnEvent")
|
||||
if err != nil {
|
||||
C.so_dlclose(handle)
|
||||
return nil, err
|
||||
}
|
||||
onUnload, err := resolve("PluginOnUnload")
|
||||
if err != nil {
|
||||
C.so_dlclose(handle)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &soPlugin{
|
||||
handle: handle,
|
||||
onLoadFn: onLoad,
|
||||
onEventFn: onEvent,
|
||||
onUnloadFn: onUnload,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type soPlugin struct {
|
||||
handle unsafe.Pointer // dlopen handle
|
||||
onLoadFn unsafe.Pointer
|
||||
onEventFn unsafe.Pointer
|
||||
onUnloadFn unsafe.Pointer
|
||||
cbHandle cgo.Handle
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (p *soPlugin) Load(send func(string, []byte), hostInfo []byte) error {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
p.cbHandle = cgo.NewHandle(send)
|
||||
|
||||
var infoPtr *C.char
|
||||
if len(hostInfo) > 0 {
|
||||
infoPtr = (*C.char)(unsafe.Pointer(&hostInfo[0]))
|
||||
}
|
||||
|
||||
ret := C.so_call_onload(p.onLoadFn, infoPtr, C.int(len(hostInfo)), C.uintptr_t(p.cbHandle))
|
||||
if ret != 0 {
|
||||
p.cbHandle.Delete()
|
||||
p.cbHandle = 0
|
||||
return errors.New("PluginOnLoad returned error")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *soPlugin) Event(event string, payload []byte) error {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
evBytes := []byte(event)
|
||||
var evPtr, plPtr *C.char
|
||||
if len(evBytes) > 0 {
|
||||
evPtr = (*C.char)(unsafe.Pointer(&evBytes[0]))
|
||||
}
|
||||
if len(payload) > 0 {
|
||||
plPtr = (*C.char)(unsafe.Pointer(&payload[0]))
|
||||
}
|
||||
|
||||
ret := C.so_call_onevent(p.onEventFn, evPtr, C.int(len(evBytes)), plPtr, C.int(len(payload)))
|
||||
if ret != 0 {
|
||||
return errors.New("PluginOnEvent returned error")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *soPlugin) Unload() {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
if p.onUnloadFn != nil {
|
||||
C.so_call_onunload(p.onUnloadFn)
|
||||
p.onUnloadFn = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (p *soPlugin) Close() error {
|
||||
p.Unload()
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
if p.cbHandle != 0 {
|
||||
p.cbHandle.Delete()
|
||||
p.cbHandle = 0
|
||||
}
|
||||
if p.handle != nil {
|
||||
C.so_dlclose(p.handle)
|
||||
p.handle = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
//go:build linux && cgo
|
||||
|
||||
package plugins
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -ldl
|
||||
#include <dlfcn.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/syscall.h>
|
||||
|
||||
#ifndef MFD_CLOEXEC
|
||||
#define MFD_CLOEXEC 0x0001U
|
||||
#endif
|
||||
|
||||
// memfd_create wrapper — creates an anonymous in-memory file descriptor.
|
||||
static int so_memfd_create(void) {
|
||||
return (int)syscall(SYS_memfd_create, "plugin", MFD_CLOEXEC);
|
||||
}
|
||||
|
||||
// Write entire buffer to fd. Returns 0 on success, -1 on error.
|
||||
static int so_write_all(int fd, const void* buf, size_t len) {
|
||||
const char* p = (const char*)buf;
|
||||
while (len > 0) {
|
||||
ssize_t n = write(fd, p, len);
|
||||
if (n <= 0) return -1;
|
||||
p += n;
|
||||
len -= (size_t)n;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Load a shared library directly from an in-memory file descriptor.
|
||||
static void* so_open_fd(int fd, char** errOut) {
|
||||
char path[64];
|
||||
snprintf(path, sizeof(path), "/proc/self/fd/%d", fd);
|
||||
void* h = dlopen(path, RTLD_NOW | RTLD_LOCAL);
|
||||
if (!h && errOut) *errOut = (char*)dlerror();
|
||||
return h;
|
||||
}
|
||||
|
||||
static void* so_dlsym(void* h, const char* name, char** errOut) {
|
||||
void* s = dlsym(h, name);
|
||||
if (!s && errOut) *errOut = (char*)dlerror();
|
||||
return s;
|
||||
}
|
||||
|
||||
static int so_dlclose(void* h) {
|
||||
if (h) return dlclose(h);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Go callback bridge — forward-declared here, implemented via //export below.
|
||||
extern void goPluginHostCallback(
|
||||
uintptr_t ctx,
|
||||
const char* event, int eventLen,
|
||||
const char* payload, int payloadLen);
|
||||
|
||||
// Call the plugin's PluginOnLoad, injecting our callback bridge + context.
|
||||
static int so_call_onload(void* fn, const char* info, int infoLen, uintptr_t ctx) {
|
||||
typedef int (*fn_t)(const char*, int, uintptr_t, uintptr_t);
|
||||
return ((fn_t)fn)(info, infoLen, (uintptr_t)goPluginHostCallback, ctx);
|
||||
}
|
||||
|
||||
static int so_call_onevent(void* fn, const char* ev, int evLen, const char* pl, int plLen) {
|
||||
typedef int (*fn_t)(const char*, int, const char*, int);
|
||||
return ((fn_t)fn)(ev, evLen, pl, plLen);
|
||||
}
|
||||
|
||||
static void so_call_onunload(void* fn) {
|
||||
typedef void (*fn_t)(void);
|
||||
((fn_t)fn)();
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"runtime/cgo"
|
||||
"sync"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
//export goPluginHostCallback
|
||||
func goPluginHostCallback(ctx C.uintptr_t, event *C.char, eventLen C.int, payload *C.char, payloadLen C.int) {
|
||||
h := cgo.Handle(ctx)
|
||||
send := h.Value().(func(string, []byte))
|
||||
ev := C.GoStringN(event, eventLen)
|
||||
var pl []byte
|
||||
if payloadLen > 0 {
|
||||
pl = C.GoBytes(unsafe.Pointer(payload), payloadLen)
|
||||
}
|
||||
send(ev, pl)
|
||||
}
|
||||
|
||||
func loadNativePlugin(data []byte) (NativePlugin, error) {
|
||||
if len(data) == 0 {
|
||||
return nil, errors.New("empty plugin binary")
|
||||
}
|
||||
|
||||
fd := C.so_memfd_create()
|
||||
if fd < 0 {
|
||||
return nil, errors.New("memfd_create failed")
|
||||
}
|
||||
|
||||
if C.so_write_all(fd, unsafe.Pointer(&data[0]), C.size_t(len(data))) != 0 {
|
||||
C.close(fd)
|
||||
return nil, errors.New("write to memfd failed")
|
||||
}
|
||||
|
||||
var cErr *C.char
|
||||
handle := C.so_open_fd(fd, &cErr)
|
||||
C.close(fd)
|
||||
if handle == nil {
|
||||
return nil, fmt.Errorf("dlopen: %s", C.GoString(cErr))
|
||||
}
|
||||
|
||||
resolve := func(name string) (unsafe.Pointer, error) {
|
||||
cName := C.CString(name)
|
||||
defer C.free(unsafe.Pointer(cName))
|
||||
var cErr *C.char
|
||||
s := C.so_dlsym(handle, cName, &cErr)
|
||||
if s == nil {
|
||||
return nil, fmt.Errorf("dlsym(%s): %s", name, C.GoString(cErr))
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
onLoad, err := resolve("PluginOnLoad")
|
||||
if err != nil {
|
||||
C.so_dlclose(handle)
|
||||
return nil, err
|
||||
}
|
||||
onEvent, err := resolve("PluginOnEvent")
|
||||
if err != nil {
|
||||
C.so_dlclose(handle)
|
||||
return nil, err
|
||||
}
|
||||
onUnload, err := resolve("PluginOnUnload")
|
||||
if err != nil {
|
||||
C.so_dlclose(handle)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &soPlugin{
|
||||
handle: handle,
|
||||
onLoadFn: onLoad,
|
||||
onEventFn: onEvent,
|
||||
onUnloadFn: onUnload,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type soPlugin struct {
|
||||
handle unsafe.Pointer
|
||||
onLoadFn unsafe.Pointer
|
||||
onEventFn unsafe.Pointer
|
||||
onUnloadFn unsafe.Pointer
|
||||
cbHandle cgo.Handle
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (p *soPlugin) Load(send func(string, []byte), hostInfo []byte) error {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
p.cbHandle = cgo.NewHandle(send)
|
||||
|
||||
var infoPtr *C.char
|
||||
if len(hostInfo) > 0 {
|
||||
infoPtr = (*C.char)(unsafe.Pointer(&hostInfo[0]))
|
||||
}
|
||||
|
||||
ret := C.so_call_onload(p.onLoadFn, infoPtr, C.int(len(hostInfo)), C.uintptr_t(p.cbHandle))
|
||||
if ret != 0 {
|
||||
p.cbHandle.Delete()
|
||||
p.cbHandle = 0
|
||||
return errors.New("PluginOnLoad returned error")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *soPlugin) Event(event string, payload []byte) error {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
evBytes := []byte(event)
|
||||
var evPtr, plPtr *C.char
|
||||
if len(evBytes) > 0 {
|
||||
evPtr = (*C.char)(unsafe.Pointer(&evBytes[0]))
|
||||
}
|
||||
if len(payload) > 0 {
|
||||
plPtr = (*C.char)(unsafe.Pointer(&payload[0]))
|
||||
}
|
||||
|
||||
ret := C.so_call_onevent(p.onEventFn, evPtr, C.int(len(evBytes)), plPtr, C.int(len(payload)))
|
||||
if ret != 0 {
|
||||
return errors.New("PluginOnEvent returned error")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *soPlugin) Unload() {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
if p.onUnloadFn != nil {
|
||||
C.so_call_onunload(p.onUnloadFn)
|
||||
p.onUnloadFn = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (p *soPlugin) Close() error {
|
||||
p.Unload()
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
if p.cbHandle != 0 {
|
||||
p.cbHandle.Delete()
|
||||
p.cbHandle = 0
|
||||
}
|
||||
if p.handle != nil {
|
||||
C.so_dlclose(p.handle)
|
||||
p.handle = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
//go:build !(linux && cgo) && !(darwin && cgo) && !windows
|
||||
|
||||
package plugins
|
||||
|
||||
import "errors"
|
||||
|
||||
func loadNativePlugin(data []byte) (NativePlugin, error) {
|
||||
return nil, errors.New("native plugins not supported on this platform (requires cgo on linux/darwin, or windows)")
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
//go:build windows
|
||||
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func loadNativePlugin(data []byte) (NativePlugin, error) {
|
||||
if len(data) == 0 {
|
||||
return nil, errors.New("empty plugin binary")
|
||||
}
|
||||
|
||||
mm, err := LoadMemoryModule(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("pe load: %w", err)
|
||||
}
|
||||
|
||||
if err := mm.CallEntryPoint(dllProcessAttach); err != nil {
|
||||
mm.Free()
|
||||
return nil, fmt.Errorf("DllMain init: %w", err)
|
||||
}
|
||||
|
||||
onLoad, err := mm.GetExport("PluginOnLoad")
|
||||
if err != nil {
|
||||
mm.Free()
|
||||
return nil, err
|
||||
}
|
||||
onEvent, err := mm.GetExport("PluginOnEvent")
|
||||
if err != nil {
|
||||
mm.Free()
|
||||
return nil, err
|
||||
}
|
||||
onUnload, err := mm.GetExport("PluginOnUnload")
|
||||
if err != nil {
|
||||
mm.Free()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
setCallback, _ := mm.GetExport("PluginSetCallback")
|
||||
|
||||
dp := &dllPlugin{
|
||||
mem: mm,
|
||||
onLoadAddr: onLoad,
|
||||
onEventAddr: onEvent,
|
||||
onUnloadAddr: onUnload,
|
||||
setCallbackAddr: setCallback,
|
||||
}
|
||||
return dp, nil
|
||||
}
|
||||
|
||||
type dllPlugin struct {
|
||||
mem *MemoryModule
|
||||
onLoadAddr uintptr
|
||||
onEventAddr uintptr
|
||||
onUnloadAddr uintptr
|
||||
setCallbackAddr uintptr
|
||||
callbackHandle uintptr // prevent GC of the callback closure
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (p *dllPlugin) Load(send func(string, []byte), hostInfo []byte) error {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
// Create a stdcall callback the DLL can invoke to send events to the host.
|
||||
cb := syscall.NewCallback(func(eventPtr, eventLen, payloadPtr, payloadLen uintptr) uintptr {
|
||||
event := make([]byte, eventLen)
|
||||
if eventLen > 0 {
|
||||
copy(event, unsafe.Slice((*byte)(unsafe.Pointer(eventPtr)), eventLen))
|
||||
}
|
||||
payload := make([]byte, payloadLen)
|
||||
if payloadLen > 0 {
|
||||
copy(payload, unsafe.Slice((*byte)(unsafe.Pointer(payloadPtr)), payloadLen))
|
||||
}
|
||||
send(string(event), payload)
|
||||
return 0
|
||||
})
|
||||
p.callbackHandle = cb
|
||||
|
||||
if p.setCallbackAddr != 0 {
|
||||
syscall.SyscallN(p.setCallbackAddr, cb)
|
||||
}
|
||||
|
||||
var infoPtr uintptr
|
||||
infoLen := uintptr(len(hostInfo))
|
||||
if len(hostInfo) > 0 {
|
||||
infoPtr = uintptr(unsafe.Pointer(&hostInfo[0]))
|
||||
}
|
||||
ret, _, _ := syscall.SyscallN(p.onLoadAddr, infoPtr, infoLen, cb)
|
||||
if int32(ret) != 0 {
|
||||
return errors.New("PluginOnLoad returned non-zero")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *dllPlugin) Event(event string, payload []byte) error {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
eventBytes := []byte(event)
|
||||
var eventPtr, payloadPtr uintptr
|
||||
eventLen := uintptr(len(eventBytes))
|
||||
payloadLen := uintptr(len(payload))
|
||||
if len(eventBytes) > 0 {
|
||||
eventPtr = uintptr(unsafe.Pointer(&eventBytes[0]))
|
||||
}
|
||||
if len(payload) > 0 {
|
||||
payloadPtr = uintptr(unsafe.Pointer(&payload[0]))
|
||||
}
|
||||
|
||||
ret, _, _ := syscall.SyscallN(p.onEventAddr, eventPtr, eventLen, payloadPtr, payloadLen)
|
||||
if int32(ret) != 0 {
|
||||
return errors.New("PluginOnEvent returned non-zero")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *dllPlugin) Unload() {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
if p.onUnloadAddr != 0 {
|
||||
syscall.SyscallN(p.onUnloadAddr)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *dllPlugin) Close() error {
|
||||
p.Unload()
|
||||
if p.mem != nil {
|
||||
p.mem.Free()
|
||||
p.mem = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -2,16 +2,11 @@ package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
"github.com/tetratelabs/wazero"
|
||||
"github.com/tetratelabs/wazero/api"
|
||||
"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
|
||||
"github.com/vmihailenco/msgpack/v5"
|
||||
|
||||
"overlord-client/cmd/agent/wire"
|
||||
)
|
||||
|
||||
@@ -26,16 +21,7 @@ type Manager struct {
|
||||
type pluginInstance struct {
|
||||
id string
|
||||
manifest PluginManifest
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
runtime wazero.Runtime
|
||||
module api.Module
|
||||
stdin io.WriteCloser
|
||||
stdout io.ReadCloser
|
||||
stderr io.ReadCloser
|
||||
enc *msgpack.Encoder
|
||||
dec *msgpack.Decoder
|
||||
encMu sync.Mutex
|
||||
native NativePlugin
|
||||
}
|
||||
|
||||
type pendingBundle struct {
|
||||
@@ -135,9 +121,9 @@ func (m *Manager) FinalizeBundle(ctx context.Context, pluginId string) error {
|
||||
return m.Load(ctx, manifest, combined)
|
||||
}
|
||||
|
||||
func (m *Manager) Load(ctx context.Context, manifest PluginManifest, wasm []byte) error {
|
||||
if len(wasm) == 0 {
|
||||
return errors.New("empty wasm payload")
|
||||
func (m *Manager) Load(ctx context.Context, manifest PluginManifest, binary []byte) error {
|
||||
if len(binary) == 0 {
|
||||
return errors.New("empty plugin binary")
|
||||
}
|
||||
pluginID := manifest.ID
|
||||
if pluginID == "" {
|
||||
@@ -146,71 +132,51 @@ func (m *Manager) Load(ctx context.Context, manifest PluginManifest, wasm []byte
|
||||
|
||||
m.mu.Lock()
|
||||
if existing, ok := m.plugins[pluginID]; ok {
|
||||
_ = existing.Close()
|
||||
existing.native.Close()
|
||||
delete(m.plugins, pluginID)
|
||||
}
|
||||
m.mu.Unlock()
|
||||
|
||||
runtimeCtx, cancel := context.WithCancel(ctx)
|
||||
rt := wazero.NewRuntime(runtimeCtx)
|
||||
if _, err := wasi_snapshot_preview1.Instantiate(runtimeCtx, rt); err != nil {
|
||||
cancel()
|
||||
return err
|
||||
}
|
||||
|
||||
stdinR, stdinW := io.Pipe()
|
||||
stdoutR, stdoutW := io.Pipe()
|
||||
stderrR, stderrW := io.Pipe()
|
||||
|
||||
config := wazero.NewModuleConfig().
|
||||
WithStdin(stdinR).
|
||||
WithStdout(stdoutW).
|
||||
WithStderr(stderrW).
|
||||
WithStartFunctions()
|
||||
|
||||
compiled, err := rt.CompileModule(runtimeCtx, wasm)
|
||||
np, err := loadNativePlugin(binary)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return err
|
||||
}
|
||||
|
||||
module, err := rt.InstantiateModule(runtimeCtx, compiled, config)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return err
|
||||
}
|
||||
|
||||
pi := &pluginInstance{
|
||||
id: pluginID,
|
||||
manifest: manifest,
|
||||
ctx: runtimeCtx,
|
||||
cancel: cancel,
|
||||
runtime: rt,
|
||||
module: module,
|
||||
stdin: stdinW,
|
||||
stdout: stdoutR,
|
||||
stderr: stderrR,
|
||||
enc: msgpack.NewEncoder(stdinW),
|
||||
dec: msgpack.NewDecoder(stdoutR),
|
||||
native: np,
|
||||
}
|
||||
|
||||
goSafe("plugin readLoop", pi.cancel, func() {
|
||||
m.readLoop(pi)
|
||||
})
|
||||
goSafe("plugin readStderr", pi.cancel, func() {
|
||||
m.readStderr(pi)
|
||||
})
|
||||
|
||||
if start := module.ExportedFunction("_start"); start != nil {
|
||||
goSafe("plugin _start", pi.cancel, func() {
|
||||
if _, err := start.Call(runtimeCtx); err != nil {
|
||||
log.Printf("[plugin] %s _start error: %v", pluginID, err)
|
||||
send := func(event string, payload []byte) {
|
||||
var payloadVal interface{}
|
||||
if len(payload) > 0 {
|
||||
var parsed interface{}
|
||||
if json.Unmarshal(payload, &parsed) == nil {
|
||||
payloadVal = parsed
|
||||
} else {
|
||||
payloadVal = string(payload)
|
||||
}
|
||||
}
|
||||
err := wire.WriteMsg(context.Background(), m.writer, wire.PluginEvent{
|
||||
Type: "plugin_event",
|
||||
PluginID: pluginID,
|
||||
Event: event,
|
||||
Payload: payloadVal,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("[plugin] %s send event error: %v", pluginID, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := m.sendInit(pi); err != nil {
|
||||
_ = pi.Close()
|
||||
hostJSON, err := json.Marshal(m.host)
|
||||
if err != nil {
|
||||
np.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
if err := np.Load(send, hostJSON); err != nil {
|
||||
np.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -218,7 +184,7 @@ func (m *Manager) Load(ctx context.Context, manifest PluginManifest, wasm []byte
|
||||
m.plugins[pluginID] = pi
|
||||
m.mu.Unlock()
|
||||
|
||||
log.Printf("[plugin] loaded %s", pluginID)
|
||||
log.Printf("[plugin] loaded %s (native)", pluginID)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -229,15 +195,19 @@ func (m *Manager) Dispatch(ctx context.Context, pluginId, event string, payload
|
||||
if pi == nil {
|
||||
return nil
|
||||
}
|
||||
msg := PluginMessage{Type: "event", Event: event, Payload: payload}
|
||||
return m.sendMessage(ctx, pi, msg)
|
||||
|
||||
data, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return pi.native.Event(event, data)
|
||||
}
|
||||
|
||||
func (m *Manager) Close() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
for id, pi := range m.plugins {
|
||||
_ = pi.Close()
|
||||
pi.native.Close()
|
||||
delete(m.plugins, id)
|
||||
}
|
||||
}
|
||||
@@ -246,103 +216,7 @@ func (m *Manager) Unload(pluginId string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if pi, ok := m.plugins[pluginId]; ok {
|
||||
_ = pi.Close()
|
||||
pi.native.Close()
|
||||
delete(m.plugins, pluginId)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) sendInit(pi *pluginInstance) error {
|
||||
payload := map[string]interface{}{
|
||||
"manifest": pi.manifest,
|
||||
"host": m.host,
|
||||
}
|
||||
msg := PluginMessage{Type: "init", Payload: payload}
|
||||
return m.sendMessage(context.Background(), pi, msg)
|
||||
}
|
||||
|
||||
func (m *Manager) sendMessage(ctx context.Context, pi *pluginInstance, msg PluginMessage) error {
|
||||
pi.encMu.Lock()
|
||||
defer pi.encMu.Unlock()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
return pi.enc.Encode(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) readLoop(pi *pluginInstance) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("[plugin] %s read loop panic: %v", pi.id, r)
|
||||
}
|
||||
}()
|
||||
for {
|
||||
var msg map[string]interface{}
|
||||
if err := pi.dec.Decode(&msg); err != nil {
|
||||
if errors.Is(err, io.EOF) || errors.Is(err, context.Canceled) {
|
||||
return
|
||||
}
|
||||
log.Printf("[plugin] %s read error: %v", pi.id, err)
|
||||
return
|
||||
}
|
||||
m.handlePluginMessage(pi.id, msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) readStderr(pi *pluginInstance) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("[plugin] %s stderr loop panic: %v", pi.id, r)
|
||||
}
|
||||
}()
|
||||
buf := make([]byte, 4096)
|
||||
for {
|
||||
n, err := pi.stderr.Read(buf)
|
||||
if n > 0 {
|
||||
log.Printf("[plugin] %s stderr: %s", pi.id, string(buf[:n]))
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) handlePluginMessage(pluginId string, msg map[string]interface{}) {
|
||||
msgType, _ := msg["type"].(string)
|
||||
if msgType == "log" {
|
||||
log.Printf("[plugin] %s log: %v", pluginId, msg["payload"])
|
||||
return
|
||||
}
|
||||
if msgType == "event" {
|
||||
event, _ := msg["event"].(string)
|
||||
payload := msg["payload"]
|
||||
if event == "" {
|
||||
return
|
||||
}
|
||||
err := wire.WriteMsg(context.Background(), m.writer, wire.PluginEvent{
|
||||
Type: "plugin_event",
|
||||
PluginID: pluginId,
|
||||
Event: event,
|
||||
Payload: payload,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("[plugin] %s send event error: %v", pluginId, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (pi *pluginInstance) Close() error {
|
||||
pi.cancel()
|
||||
_ = pi.stdin.Close()
|
||||
_ = pi.stdout.Close()
|
||||
_ = pi.stderr.Close()
|
||||
if pi.module != nil {
|
||||
_ = pi.module.Close(context.Background())
|
||||
}
|
||||
if pi.runtime != nil {
|
||||
_ = pi.runtime.Close(context.Background())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,524 @@
|
||||
//go:build windows
|
||||
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const (
|
||||
imageDOSSignature = 0x5A4D // "MZ"
|
||||
imageNTSignature = 0x00004550 // "PE\0\0"
|
||||
|
||||
imageOptionalHdrMagicPE32Plus = 0x20B
|
||||
|
||||
imageDirectoryEntryExport = 0
|
||||
imageDirectoryEntryImport = 1
|
||||
imageDirectoryEntryBaseReloc = 5
|
||||
imageDirectoryEntryTLS = 9
|
||||
|
||||
imageRelBasedDir64 = 10
|
||||
imageRelBasedHighLow = 3
|
||||
imageRelBasedAbsolute = 0
|
||||
|
||||
imageSCNMemExecute = 0x20000000
|
||||
imageSCNMemRead = 0x40000000
|
||||
imageSCNMemWrite = 0x80000000
|
||||
|
||||
dllProcessAttach = 1
|
||||
dllProcessDetach = 0
|
||||
)
|
||||
|
||||
type imageDOSHeader struct {
|
||||
Magic uint16
|
||||
_ [28]uint16
|
||||
LfaNew int32
|
||||
}
|
||||
|
||||
type imageFileHeader struct {
|
||||
Machine uint16
|
||||
NumberOfSections uint16
|
||||
TimeDateStamp uint32
|
||||
PointerToSymbolTable uint32
|
||||
NumberOfSymbols uint32
|
||||
SizeOfOptionalHeader uint16
|
||||
Characteristics uint16
|
||||
}
|
||||
|
||||
type imageDataDirectory struct {
|
||||
VirtualAddress uint32
|
||||
Size uint32
|
||||
}
|
||||
|
||||
type imageOptionalHeader64 struct {
|
||||
Magic uint16
|
||||
_ [14]byte
|
||||
AddressOfEntryPoint uint32
|
||||
_ [4]byte
|
||||
ImageBase uint64
|
||||
SectionAlignment uint32
|
||||
FileAlignment uint32
|
||||
_ [16]byte
|
||||
SizeOfImage uint32
|
||||
SizeOfHeaders uint32
|
||||
_ [4]byte
|
||||
_ [4]byte // Subsystem + DllCharacteristics
|
||||
_ [36]byte
|
||||
NumberOfRvaAndSizes uint32
|
||||
DataDirectory [16]imageDataDirectory
|
||||
}
|
||||
|
||||
type imageNTHeaders64 struct {
|
||||
Signature uint32
|
||||
FileHeader imageFileHeader
|
||||
OptionalHeader imageOptionalHeader64
|
||||
}
|
||||
|
||||
type imageSectionHeader struct {
|
||||
Name [8]byte
|
||||
VirtualSize uint32
|
||||
VirtualAddress uint32
|
||||
SizeOfRawData uint32
|
||||
PointerToRawData uint32
|
||||
_ [12]byte
|
||||
Characteristics uint32
|
||||
}
|
||||
|
||||
type imageImportDescriptor struct {
|
||||
OriginalFirstThunk uint32
|
||||
TimeDateStamp uint32
|
||||
ForwarderChain uint32
|
||||
Name uint32
|
||||
FirstThunk uint32
|
||||
}
|
||||
|
||||
type imageBaseRelocation struct {
|
||||
VirtualAddress uint32
|
||||
SizeOfBlock uint32
|
||||
}
|
||||
|
||||
type imageExportDirectory struct {
|
||||
_ [12]byte
|
||||
Name uint32
|
||||
Base uint32
|
||||
NumberOfFunctions uint32
|
||||
NumberOfNames uint32
|
||||
AddressOfFunctions uint32
|
||||
AddressOfNames uint32
|
||||
AddressOfNameOrdinals uint32
|
||||
}
|
||||
|
||||
type imageTLSDirectory64 struct {
|
||||
StartAddressOfRawData uint64
|
||||
EndAddressOfRawData uint64
|
||||
AddressOfIndex uint64
|
||||
AddressOfCallBacks uint64
|
||||
SizeOfZeroFill uint32
|
||||
Characteristics uint32
|
||||
}
|
||||
|
||||
type MemoryModule struct {
|
||||
base uintptr
|
||||
size uintptr
|
||||
entryPoint uintptr
|
||||
exports map[string]uintptr
|
||||
importDLLs []windows.Handle
|
||||
initialized bool
|
||||
tlsIndex uint32
|
||||
tlsData uintptr
|
||||
}
|
||||
|
||||
func LoadMemoryModule(data []byte) (*MemoryModule, error) {
|
||||
if len(data) < int(unsafe.Sizeof(imageDOSHeader{})) {
|
||||
return nil, errors.New("pe: data too small for DOS header")
|
||||
}
|
||||
|
||||
dosHdr := (*imageDOSHeader)(unsafe.Pointer(&data[0]))
|
||||
if dosHdr.Magic != imageDOSSignature {
|
||||
return nil, errors.New("pe: invalid DOS signature")
|
||||
}
|
||||
|
||||
ntOffset := int(dosHdr.LfaNew)
|
||||
if ntOffset < 0 || ntOffset+int(unsafe.Sizeof(imageNTHeaders64{})) > len(data) {
|
||||
return nil, errors.New("pe: NT headers out of bounds")
|
||||
}
|
||||
ntHdr := (*imageNTHeaders64)(unsafe.Pointer(&data[ntOffset]))
|
||||
if ntHdr.Signature != imageNTSignature {
|
||||
return nil, errors.New("pe: invalid NT signature")
|
||||
}
|
||||
if ntHdr.OptionalHeader.Magic != imageOptionalHdrMagicPE32Plus {
|
||||
return nil, errors.New("pe: only PE32+ (64-bit) is supported")
|
||||
}
|
||||
|
||||
imageSize := uintptr(ntHdr.OptionalHeader.SizeOfImage)
|
||||
preferredBase := uintptr(ntHdr.OptionalHeader.ImageBase)
|
||||
|
||||
base, err := windows.VirtualAlloc(preferredBase, imageSize, windows.MEM_RESERVE|windows.MEM_COMMIT, windows.PAGE_READWRITE)
|
||||
if err != nil || base == 0 {
|
||||
base, err = windows.VirtualAlloc(0, imageSize, windows.MEM_RESERVE|windows.MEM_COMMIT, windows.PAGE_READWRITE)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("pe: VirtualAlloc failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
mm := &MemoryModule{
|
||||
base: base,
|
||||
size: imageSize,
|
||||
exports: make(map[string]uintptr),
|
||||
tlsIndex: 0xFFFFFFFF,
|
||||
}
|
||||
|
||||
headerSize := uintptr(ntHdr.OptionalHeader.SizeOfHeaders)
|
||||
if headerSize > uintptr(len(data)) {
|
||||
headerSize = uintptr(len(data))
|
||||
}
|
||||
copyMem(base, &data[0], headerSize)
|
||||
|
||||
sectionStart := ntOffset + int(unsafe.Sizeof(ntHdr.Signature)) +
|
||||
int(unsafe.Sizeof(ntHdr.FileHeader)) +
|
||||
int(ntHdr.FileHeader.SizeOfOptionalHeader)
|
||||
|
||||
for i := 0; i < int(ntHdr.FileHeader.NumberOfSections); i++ {
|
||||
off := sectionStart + i*int(unsafe.Sizeof(imageSectionHeader{}))
|
||||
if off+int(unsafe.Sizeof(imageSectionHeader{})) > len(data) {
|
||||
mm.Free()
|
||||
return nil, errors.New("pe: section header out of bounds")
|
||||
}
|
||||
sec := (*imageSectionHeader)(unsafe.Pointer(&data[off]))
|
||||
if sec.SizeOfRawData > 0 {
|
||||
rawOff := int(sec.PointerToRawData)
|
||||
rawEnd := rawOff + int(sec.SizeOfRawData)
|
||||
if rawEnd > len(data) {
|
||||
mm.Free()
|
||||
return nil, errors.New("pe: section raw data out of bounds")
|
||||
}
|
||||
copySize := uintptr(sec.SizeOfRawData)
|
||||
if sec.VirtualSize > 0 && uintptr(sec.VirtualSize) < copySize {
|
||||
copySize = uintptr(sec.VirtualSize)
|
||||
}
|
||||
dest := uintptr(sec.VirtualAddress)
|
||||
if dest+copySize > imageSize {
|
||||
if dest >= imageSize {
|
||||
continue
|
||||
}
|
||||
copySize = imageSize - dest
|
||||
}
|
||||
copyMem(base+dest, &data[rawOff], copySize)
|
||||
}
|
||||
}
|
||||
|
||||
mappedNT := (*imageNTHeaders64)(unsafe.Pointer(base + uintptr(dosHdr.LfaNew)))
|
||||
delta := int64(base) - int64(mappedNT.OptionalHeader.ImageBase)
|
||||
|
||||
if delta != 0 {
|
||||
relocDir := mappedNT.OptionalHeader.DataDirectory[imageDirectoryEntryBaseReloc]
|
||||
if relocDir.VirtualAddress != 0 && relocDir.Size != 0 {
|
||||
if err := mm.processRelocations(relocDir, delta); err != nil {
|
||||
mm.Free()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
importDir := mappedNT.OptionalHeader.DataDirectory[imageDirectoryEntryImport]
|
||||
if importDir.VirtualAddress != 0 && importDir.Size != 0 {
|
||||
if err := mm.resolveImports(importDir); err != nil {
|
||||
mm.Free()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < int(ntHdr.FileHeader.NumberOfSections); i++ {
|
||||
off := sectionStart + i*int(unsafe.Sizeof(imageSectionHeader{}))
|
||||
sec := (*imageSectionHeader)(unsafe.Pointer(&data[off]))
|
||||
prot := sectionProtection(sec.Characteristics)
|
||||
size := uintptr(sec.VirtualSize)
|
||||
if size == 0 {
|
||||
size = uintptr(sec.SizeOfRawData)
|
||||
}
|
||||
if size == 0 {
|
||||
continue
|
||||
}
|
||||
var oldProt uint32
|
||||
_ = windows.VirtualProtect(base+uintptr(sec.VirtualAddress), size, prot, &oldProt)
|
||||
}
|
||||
|
||||
mm.setupTLS(dllProcessAttach)
|
||||
|
||||
if mappedNT.OptionalHeader.AddressOfEntryPoint != 0 {
|
||||
mm.entryPoint = base + uintptr(mappedNT.OptionalHeader.AddressOfEntryPoint)
|
||||
}
|
||||
|
||||
exportDir := mappedNT.OptionalHeader.DataDirectory[imageDirectoryEntryExport]
|
||||
if exportDir.VirtualAddress != 0 && exportDir.Size != 0 {
|
||||
mm.parseExports(exportDir)
|
||||
}
|
||||
|
||||
return mm, nil
|
||||
}
|
||||
|
||||
func (mm *MemoryModule) CallEntryPoint(reason uint32) error {
|
||||
if mm.entryPoint == 0 {
|
||||
return nil
|
||||
}
|
||||
ret, _, _ := syscall.SyscallN(mm.entryPoint, mm.base, uintptr(reason), 0)
|
||||
if ret == 0 && reason == dllProcessAttach {
|
||||
return errors.New("pe: DllMain returned FALSE")
|
||||
}
|
||||
mm.initialized = (reason == dllProcessAttach)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mm *MemoryModule) GetExport(name string) (uintptr, error) {
|
||||
addr, ok := mm.exports[name]
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("pe: export %q not found", name)
|
||||
}
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
func (mm *MemoryModule) Free() {
|
||||
if mm.base == 0 {
|
||||
return
|
||||
}
|
||||
if mm.initialized && mm.entryPoint != 0 {
|
||||
syscall.SyscallN(mm.entryPoint, mm.base, dllProcessDetach, 0)
|
||||
mm.initialized = false
|
||||
}
|
||||
if mm.tlsIndex != 0xFFFFFFFF {
|
||||
if mm.tlsData != 0 {
|
||||
_ = windows.VirtualFree(mm.tlsData, 0, windows.MEM_RELEASE)
|
||||
}
|
||||
procTlsFree.Call(uintptr(mm.tlsIndex))
|
||||
}
|
||||
for _, h := range mm.importDLLs {
|
||||
_ = windows.FreeLibrary(h)
|
||||
}
|
||||
mm.importDLLs = nil
|
||||
_ = windows.VirtualFree(mm.base, 0, windows.MEM_RELEASE)
|
||||
mm.base = 0
|
||||
}
|
||||
|
||||
func (mm *MemoryModule) processRelocations(dir imageDataDirectory, delta int64) error {
|
||||
offset := uintptr(dir.VirtualAddress)
|
||||
end := offset + uintptr(dir.Size)
|
||||
|
||||
for offset < end {
|
||||
block := (*imageBaseRelocation)(unsafe.Pointer(mm.base + offset))
|
||||
if block.SizeOfBlock == 0 {
|
||||
break
|
||||
}
|
||||
count := (block.SizeOfBlock - 8) / 2
|
||||
entries := mm.base + offset + 8
|
||||
|
||||
for i := uint32(0); i < count; i++ {
|
||||
entry := *(*uint16)(unsafe.Pointer(entries + uintptr(i)*2))
|
||||
typ := entry >> 12
|
||||
off := uintptr(entry & 0xFFF)
|
||||
addr := mm.base + uintptr(block.VirtualAddress) + off
|
||||
|
||||
switch typ {
|
||||
case imageRelBasedAbsolute:
|
||||
case imageRelBasedHighLow:
|
||||
val := (*uint32)(unsafe.Pointer(addr))
|
||||
*val = uint32(int64(*val) + delta)
|
||||
case imageRelBasedDir64:
|
||||
val := (*uint64)(unsafe.Pointer(addr))
|
||||
*val = uint64(int64(*val) + delta)
|
||||
default:
|
||||
return fmt.Errorf("pe: unsupported relocation type %d", typ)
|
||||
}
|
||||
}
|
||||
offset += uintptr(block.SizeOfBlock)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mm *MemoryModule) resolveImports(dir imageDataDirectory) error {
|
||||
descSize := unsafe.Sizeof(imageImportDescriptor{})
|
||||
offset := uintptr(dir.VirtualAddress)
|
||||
|
||||
for {
|
||||
desc := (*imageImportDescriptor)(unsafe.Pointer(mm.base + offset))
|
||||
if desc.Name == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
dllName := peString(mm.base + uintptr(desc.Name))
|
||||
hDLL, err := windows.LoadLibrary(dllName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("pe: LoadLibrary(%s): %w", dllName, err)
|
||||
}
|
||||
mm.importDLLs = append(mm.importDLLs, hDLL)
|
||||
|
||||
thunkRef := mm.base + uintptr(desc.OriginalFirstThunk)
|
||||
thunkAddr := mm.base + uintptr(desc.FirstThunk)
|
||||
if desc.OriginalFirstThunk == 0 {
|
||||
thunkRef = thunkAddr
|
||||
}
|
||||
|
||||
for {
|
||||
ref := *(*uint64)(unsafe.Pointer(thunkRef))
|
||||
if ref == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
var procAddr uintptr
|
||||
if ref&(1<<63) != 0 {
|
||||
ordinal := uint16(ref & 0xFFFF)
|
||||
procAddr, err = getProcByOrdinal(hDLL, ordinal)
|
||||
} else {
|
||||
nameAddr := mm.base + uintptr(ref) + 2
|
||||
funcName := peString(nameAddr)
|
||||
procAddr, err = windows.GetProcAddress(hDLL, funcName)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("pe: import resolve from %s: %w", dllName, err)
|
||||
}
|
||||
|
||||
*(*uintptr)(unsafe.Pointer(thunkAddr)) = procAddr
|
||||
thunkRef += 8
|
||||
thunkAddr += 8
|
||||
}
|
||||
offset += descSize
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mm *MemoryModule) parseExports(dir imageDataDirectory) {
|
||||
if dir.Size == 0 {
|
||||
return
|
||||
}
|
||||
expDir := (*imageExportDirectory)(unsafe.Pointer(mm.base + uintptr(dir.VirtualAddress)))
|
||||
numNames := int(expDir.NumberOfNames)
|
||||
if numNames == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
namesRVA := mm.base + uintptr(expDir.AddressOfNames)
|
||||
ordinalsRVA := mm.base + uintptr(expDir.AddressOfNameOrdinals)
|
||||
funcsRVA := mm.base + uintptr(expDir.AddressOfFunctions)
|
||||
|
||||
exportStart := uintptr(dir.VirtualAddress)
|
||||
exportEnd := exportStart + uintptr(dir.Size)
|
||||
|
||||
for i := 0; i < numNames; i++ {
|
||||
nameRVA := *(*uint32)(unsafe.Pointer(namesRVA + uintptr(i)*4))
|
||||
ordinal := *(*uint16)(unsafe.Pointer(ordinalsRVA + uintptr(i)*2))
|
||||
funcRVA := *(*uint32)(unsafe.Pointer(funcsRVA + uintptr(ordinal)*4))
|
||||
|
||||
if uintptr(funcRVA) >= exportStart && uintptr(funcRVA) < exportEnd {
|
||||
continue
|
||||
}
|
||||
|
||||
name := peString(mm.base + uintptr(nameRVA))
|
||||
mm.exports[name] = mm.base + uintptr(funcRVA)
|
||||
}
|
||||
}
|
||||
|
||||
func (mm *MemoryModule) setupTLS(reason uint32) {
|
||||
dosHdr := (*imageDOSHeader)(unsafe.Pointer(mm.base))
|
||||
ntHdr := (*imageNTHeaders64)(unsafe.Pointer(mm.base + uintptr(dosHdr.LfaNew)))
|
||||
tlsDir := ntHdr.OptionalHeader.DataDirectory[imageDirectoryEntryTLS]
|
||||
if tlsDir.VirtualAddress == 0 || tlsDir.Size == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
tls := (*imageTLSDirectory64)(unsafe.Pointer(mm.base + uintptr(tlsDir.VirtualAddress)))
|
||||
|
||||
idx, _, _ := procTlsAlloc.Call()
|
||||
if idx == 0xFFFFFFFF {
|
||||
return
|
||||
}
|
||||
mm.tlsIndex = uint32(idx)
|
||||
|
||||
if tls.AddressOfIndex != 0 {
|
||||
*(*uint32)(unsafe.Pointer(uintptr(tls.AddressOfIndex))) = mm.tlsIndex
|
||||
}
|
||||
|
||||
dataSize := uintptr(tls.EndAddressOfRawData - tls.StartAddressOfRawData)
|
||||
totalSize := dataSize + uintptr(tls.SizeOfZeroFill)
|
||||
if totalSize > 0 {
|
||||
tlsData, err := windows.VirtualAlloc(0, totalSize,
|
||||
windows.MEM_RESERVE|windows.MEM_COMMIT, windows.PAGE_READWRITE)
|
||||
if err == nil && tlsData != 0 {
|
||||
if dataSize > 0 {
|
||||
copyMem(tlsData, (*byte)(unsafe.Pointer(uintptr(tls.StartAddressOfRawData))), dataSize)
|
||||
}
|
||||
procTlsSetValue.Call(uintptr(mm.tlsIndex), tlsData)
|
||||
mm.tlsData = tlsData
|
||||
}
|
||||
}
|
||||
|
||||
if tls.AddressOfCallBacks != 0 {
|
||||
cbAddr := uintptr(tls.AddressOfCallBacks)
|
||||
for {
|
||||
cb := *(*uintptr)(unsafe.Pointer(cbAddr))
|
||||
if cb == 0 {
|
||||
break
|
||||
}
|
||||
syscall.SyscallN(cb, mm.base, uintptr(reason), 0)
|
||||
cbAddr += 8
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func peString(addr uintptr) string {
|
||||
var buf []byte
|
||||
for i := 0; i < 4096; i++ { // safety limit
|
||||
b := *(*byte)(unsafe.Pointer(addr + uintptr(i)))
|
||||
if b == 0 {
|
||||
break
|
||||
}
|
||||
buf = append(buf, b)
|
||||
}
|
||||
return string(buf)
|
||||
}
|
||||
|
||||
var (
|
||||
modKernel32 = windows.NewLazySystemDLL("kernel32.dll")
|
||||
procGetProcAddr = modKernel32.NewProc("GetProcAddress")
|
||||
procTlsAlloc = modKernel32.NewProc("TlsAlloc")
|
||||
procTlsSetValue = modKernel32.NewProc("TlsSetValue")
|
||||
procTlsFree = modKernel32.NewProc("TlsFree")
|
||||
)
|
||||
|
||||
func getProcByOrdinal(module windows.Handle, ordinal uint16) (uintptr, error) {
|
||||
r, _, err := procGetProcAddr.Call(uintptr(module), uintptr(ordinal))
|
||||
if r == 0 {
|
||||
return 0, fmt.Errorf("GetProcAddress ordinal %d: %w", ordinal, err)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func sectionProtection(chars uint32) uint32 {
|
||||
exec := chars&imageSCNMemExecute != 0
|
||||
read := chars&imageSCNMemRead != 0
|
||||
write := chars&imageSCNMemWrite != 0
|
||||
|
||||
switch {
|
||||
case exec && write:
|
||||
return windows.PAGE_EXECUTE_READWRITE
|
||||
case exec && read:
|
||||
return windows.PAGE_EXECUTE_READ
|
||||
case exec:
|
||||
return windows.PAGE_EXECUTE
|
||||
case write:
|
||||
return windows.PAGE_READWRITE
|
||||
case read:
|
||||
return windows.PAGE_READONLY
|
||||
default:
|
||||
return windows.PAGE_NOACCESS
|
||||
}
|
||||
}
|
||||
|
||||
func copyMem(dst uintptr, src *byte, size uintptr) {
|
||||
dstSlice := unsafe.Slice((*byte)(unsafe.Pointer(dst)), size)
|
||||
srcSlice := unsafe.Slice(src, size)
|
||||
copy(dstSlice, srcSlice)
|
||||
}
|
||||
@@ -9,13 +9,14 @@ type PluginAssets struct {
|
||||
}
|
||||
|
||||
type PluginManifest struct {
|
||||
ID string `msgpack:"id" json:"id"`
|
||||
Name string `msgpack:"name" json:"name"`
|
||||
Version string `msgpack:"version,omitempty" json:"version,omitempty"`
|
||||
Description string `msgpack:"description,omitempty" json:"description,omitempty"`
|
||||
Binary string `msgpack:"binary,omitempty" json:"binary,omitempty"`
|
||||
Entry string `msgpack:"entry,omitempty" json:"entry,omitempty"`
|
||||
Assets PluginAssets `msgpack:"assets,omitempty" json:"assets,omitempty"`
|
||||
ID string `msgpack:"id" json:"id"`
|
||||
Name string `msgpack:"name" json:"name"`
|
||||
Version string `msgpack:"version,omitempty" json:"version,omitempty"`
|
||||
Description string `msgpack:"description,omitempty" json:"description,omitempty"`
|
||||
Binary string `msgpack:"binary,omitempty" json:"binary,omitempty"`
|
||||
Binaries map[string]string `msgpack:"binaries,omitempty" json:"binaries,omitempty"`
|
||||
Entry string `msgpack:"entry,omitempty" json:"entry,omitempty"`
|
||||
Assets PluginAssets `msgpack:"assets,omitempty" json:"assets,omitempty"`
|
||||
}
|
||||
|
||||
type PluginMessage struct {
|
||||
@@ -26,10 +27,20 @@ type PluginMessage struct {
|
||||
}
|
||||
|
||||
type HostInfo struct {
|
||||
ClientID string `msgpack:"clientId"`
|
||||
OS string `msgpack:"os"`
|
||||
Arch string `msgpack:"arch"`
|
||||
Version string `msgpack:"version"`
|
||||
ClientID string `msgpack:"clientId" json:"clientId"`
|
||||
OS string `msgpack:"os" json:"os"`
|
||||
Arch string `msgpack:"arch" json:"arch"`
|
||||
Version string `msgpack:"version" json:"version"`
|
||||
}
|
||||
|
||||
type NativePlugin interface {
|
||||
Load(send func(event string, payload []byte), hostInfo []byte) error
|
||||
|
||||
Event(event string, payload []byte) error
|
||||
|
||||
Unload()
|
||||
|
||||
Close() error
|
||||
}
|
||||
|
||||
func ManifestFromMap(m map[string]interface{}) (PluginManifest, error) {
|
||||
@@ -41,6 +52,15 @@ func ManifestFromMap(m map[string]interface{}) (PluginManifest, error) {
|
||||
manifest.Binary = stringVal(m["binary"])
|
||||
manifest.Entry = stringVal(m["entry"])
|
||||
|
||||
if binariesRaw, ok := m["binaries"].(map[string]interface{}); ok {
|
||||
manifest.Binaries = make(map[string]string, len(binariesRaw))
|
||||
for k, v := range binariesRaw {
|
||||
if s, ok := v.(string); ok {
|
||||
manifest.Binaries[k] = s
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if assetsRaw, ok := m["assets"].(map[string]interface{}); ok {
|
||||
manifest.Assets = PluginAssets{
|
||||
HTML: stringVal(assetsRaw["html"]),
|
||||
|
||||
@@ -7,7 +7,6 @@ require (
|
||||
github.com/gen2brain/x264-go v0.3.1 // optional: used when cgo is enabled
|
||||
github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018
|
||||
github.com/pixiv/go-libjpeg v0.0.0-20190822045933-3da21a74767d // optional: turbojpeg build tag
|
||||
github.com/tetratelabs/wazero v1.7.0
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1
|
||||
nhooyr.io/websocket v1.8.10
|
||||
)
|
||||
|
||||
@@ -28,8 +28,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/tetratelabs/wazero v1.7.0 h1:jg5qPydno59wqjpGrHph81lbtHzTrWzwwtD4cD88+hQ=
|
||||
github.com/tetratelabs/wazero v1.7.0/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||
|
||||
Reference in New Issue
Block a user