dll plugin loading heavily improved
This commit is contained in:
@@ -74,10 +74,11 @@ func goPluginHostCallback(ctx C.uintptr_t, event *C.char, eventLen C.int, payloa
|
|||||||
send(ev, pl)
|
send(ev, pl)
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadNativePlugin(data []byte) (NativePlugin, error) {
|
func loadNativePlugin(manifest PluginManifest, data []byte) (NativePlugin, error) {
|
||||||
if len(data) == 0 {
|
if len(data) == 0 {
|
||||||
return nil, errors.New("empty plugin binary")
|
return nil, errors.New("empty plugin binary")
|
||||||
}
|
}
|
||||||
|
entries := nativeEntries(manifest)
|
||||||
|
|
||||||
f, err := os.CreateTemp("", "*.dylib")
|
f, err := os.CreateTemp("", "*.dylib")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -114,24 +115,24 @@ func loadNativePlugin(data []byte) (NativePlugin, error) {
|
|||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
onLoad, err := resolve("PluginOnLoad")
|
onLoad, err := resolve(entries.onLoad)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
C.so_dlclose(handle)
|
C.so_dlclose(handle)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
onEvent, err := resolve("PluginOnEvent")
|
onEvent, err := resolve(entries.onEvent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
C.so_dlclose(handle)
|
C.so_dlclose(handle)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
onUnload, err := resolve("PluginOnUnload")
|
onUnload, err := resolve(entries.onUnload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
C.so_dlclose(handle)
|
C.so_dlclose(handle)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
pluginRT := "go"
|
pluginRT := "go"
|
||||||
if getRuntimeFn, err := resolve("PluginGetRuntime"); err == nil {
|
if getRuntimeFn, err := resolve(entries.getRuntime); err == nil {
|
||||||
if cs := C.so_call_getruntime(getRuntimeFn); cs != nil {
|
if cs := C.so_call_getruntime(getRuntimeFn); cs != nil {
|
||||||
pluginRT = C.GoString(cs)
|
pluginRT = C.GoString(cs)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,10 +101,11 @@ func goPluginHostCallback(ctx C.uintptr_t, event *C.char, eventLen C.int, payloa
|
|||||||
send(ev, pl)
|
send(ev, pl)
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadNativePlugin(data []byte) (NativePlugin, error) {
|
func loadNativePlugin(manifest PluginManifest, data []byte) (NativePlugin, error) {
|
||||||
if len(data) == 0 {
|
if len(data) == 0 {
|
||||||
return nil, errors.New("empty plugin binary")
|
return nil, errors.New("empty plugin binary")
|
||||||
}
|
}
|
||||||
|
entries := nativeEntries(manifest)
|
||||||
|
|
||||||
// Prefer the subprocess shim approach: the main agent binary is statically
|
// Prefer the subprocess shim approach: the main agent binary is statically
|
||||||
// linked (musl -static) which prevents dlopen from working in-process.
|
// linked (musl -static) which prevents dlopen from working in-process.
|
||||||
@@ -145,24 +146,24 @@ func loadNativePlugin(data []byte) (NativePlugin, error) {
|
|||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
onLoad, err := resolve("PluginOnLoad")
|
onLoad, err := resolve(entries.onLoad)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
C.so_dlclose(handle)
|
C.so_dlclose(handle)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
onEvent, err := resolve("PluginOnEvent")
|
onEvent, err := resolve(entries.onEvent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
C.so_dlclose(handle)
|
C.so_dlclose(handle)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
onUnload, err := resolve("PluginOnUnload")
|
onUnload, err := resolve(entries.onUnload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
C.so_dlclose(handle)
|
C.so_dlclose(handle)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
pluginRT := "go"
|
pluginRT := "go"
|
||||||
if getRuntimeFn, err := resolve("PluginGetRuntime"); err == nil {
|
if getRuntimeFn, err := resolve(entries.getRuntime); err == nil {
|
||||||
if cs := C.so_call_getruntime(getRuntimeFn); cs != nil {
|
if cs := C.so_call_getruntime(getRuntimeFn); cs != nil {
|
||||||
pluginRT = C.GoString(cs)
|
pluginRT = C.GoString(cs)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,6 @@ package plugins
|
|||||||
|
|
||||||
import "errors"
|
import "errors"
|
||||||
|
|
||||||
func loadNativePlugin(data []byte) (NativePlugin, error) {
|
func loadNativePlugin(manifest PluginManifest, data []byte) (NativePlugin, error) {
|
||||||
return nil, errors.New("native plugins not supported on this platform (requires cgo on linux/darwin, or windows)")
|
return nil, errors.New("native plugins not supported on this platform (requires cgo on linux/darwin, or windows)")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,18 +3,49 @@
|
|||||||
package plugins
|
package plugins
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
)
|
)
|
||||||
|
|
||||||
func loadNativePlugin(data []byte) (NativePlugin, error) {
|
const (
|
||||||
|
loadLibrarySearchDLLLoadDir = 0x00000100
|
||||||
|
loadLibrarySearchDefaultDirs = 0x00001000
|
||||||
|
)
|
||||||
|
|
||||||
|
var procLoadLibraryExW = modKernel32.NewProc("LoadLibraryExW")
|
||||||
|
|
||||||
|
func loadNativePlugin(manifest PluginManifest, data []byte) (NativePlugin, error) {
|
||||||
if len(data) == 0 {
|
if len(data) == 0 {
|
||||||
return nil, errors.New("empty plugin binary")
|
return nil, errors.New("empty plugin binary")
|
||||||
}
|
}
|
||||||
|
if shouldUseOSNativeLoader(manifest.NativeLoader) {
|
||||||
|
return loadNativePluginOS(manifest, data)
|
||||||
|
}
|
||||||
|
return loadNativePluginMemory(manifest, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldUseOSNativeLoader(loader string) bool {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(loader)) {
|
||||||
|
case "os", "disk", "file", "loadlibrary", "loadlibraryex":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadNativePluginMemory(manifest PluginManifest, data []byte) (NativePlugin, error) {
|
||||||
|
entries := nativeEntries(manifest)
|
||||||
|
|
||||||
type initResult struct {
|
type initResult struct {
|
||||||
dp *dllPlugin
|
dp *dllPlugin
|
||||||
@@ -44,21 +75,21 @@ func loadNativePlugin(data []byte) (NativePlugin, error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
onLoad, err := mm.GetExport("PluginOnLoad")
|
onLoad, err := mm.GetExport(entries.onLoad)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mm.Free()
|
mm.Free()
|
||||||
ch <- initResult{err: err}
|
ch <- initResult{err: err}
|
||||||
runtime.UnlockOSThread()
|
runtime.UnlockOSThread()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
onEvent, err := mm.GetExport("PluginOnEvent")
|
onEvent, err := mm.GetExport(entries.onEvent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mm.Free()
|
mm.Free()
|
||||||
ch <- initResult{err: err}
|
ch <- initResult{err: err}
|
||||||
runtime.UnlockOSThread()
|
runtime.UnlockOSThread()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
onUnload, err := mm.GetExport("PluginOnUnload")
|
onUnload, err := mm.GetExport(entries.onUnload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mm.Free()
|
mm.Free()
|
||||||
ch <- initResult{err: err}
|
ch <- initResult{err: err}
|
||||||
@@ -66,10 +97,10 @@ func loadNativePlugin(data []byte) (NativePlugin, error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setCallback, _ := mm.GetExport("PluginSetCallback")
|
setCallback, _ := mm.GetExport(entries.setCallback)
|
||||||
|
|
||||||
rt := "go"
|
rt := "go"
|
||||||
if getRuntimeAddr, err := mm.GetExport("PluginGetRuntime"); err == nil {
|
if getRuntimeAddr, err := mm.GetExport(entries.getRuntime); err == nil {
|
||||||
ret, _, _ := syscall.SyscallN(getRuntimeAddr)
|
ret, _, _ := syscall.SyscallN(getRuntimeAddr)
|
||||||
if ret != 0 {
|
if ret != 0 {
|
||||||
var buf [32]byte
|
var buf [32]byte
|
||||||
@@ -108,6 +139,125 @@ func loadNativePlugin(data []byte) (NativePlugin, error) {
|
|||||||
return res.dp, nil
|
return res.dp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadNativePluginOS(manifest PluginManifest, data []byte) (NativePlugin, error) {
|
||||||
|
entries := nativeEntries(manifest)
|
||||||
|
dllPath, err := stageNativePluginDLL(manifest, data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
path16, err := windows.UTF16PtrFromString(dllPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
flags := uintptr(loadLibrarySearchDLLLoadDir | loadLibrarySearchDefaultDirs)
|
||||||
|
h, _, callErr := procLoadLibraryExW.Call(uintptr(unsafe.Pointer(path16)), 0, flags)
|
||||||
|
if h == 0 {
|
||||||
|
return nil, fmt.Errorf("LoadLibraryExW(%s): %w", dllPath, callErr)
|
||||||
|
}
|
||||||
|
module := windows.Handle(h)
|
||||||
|
|
||||||
|
resolve := func(name string, required bool) (uintptr, error) {
|
||||||
|
addr, err := windows.GetProcAddress(module, name)
|
||||||
|
if err != nil {
|
||||||
|
if required {
|
||||||
|
return 0, fmt.Errorf("GetProcAddress(%s): %w", name, err)
|
||||||
|
}
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
return addr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoad, err := resolve(entries.onLoad, true)
|
||||||
|
if err != nil {
|
||||||
|
_ = windows.FreeLibrary(module)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
onEvent, err := resolve(entries.onEvent, true)
|
||||||
|
if err != nil {
|
||||||
|
_ = windows.FreeLibrary(module)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
onUnload, err := resolve(entries.onUnload, true)
|
||||||
|
if err != nil {
|
||||||
|
_ = windows.FreeLibrary(module)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
setCallback, _ := resolve(entries.setCallback, false)
|
||||||
|
|
||||||
|
rt := "go"
|
||||||
|
if getRuntimeAddr, err := resolve(entries.getRuntime, false); err == nil && getRuntimeAddr != 0 {
|
||||||
|
ret, _, _ := syscall.SyscallN(getRuntimeAddr)
|
||||||
|
if ret != 0 {
|
||||||
|
var buf [32]byte
|
||||||
|
for i := range buf {
|
||||||
|
b := *(*byte)(unsafe.Pointer(ret + uintptr(i)))
|
||||||
|
if b == 0 {
|
||||||
|
rt = string(buf[:i])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
buf[i] = b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dp := &dllPlugin{
|
||||||
|
module: module,
|
||||||
|
modulePath: dllPath,
|
||||||
|
onLoadAddr: onLoad,
|
||||||
|
onEventAddr: onEvent,
|
||||||
|
onUnloadAddr: onUnload,
|
||||||
|
setCallbackAddr: setCallback,
|
||||||
|
pluginRuntime: rt,
|
||||||
|
workCh: make(chan pluginWork),
|
||||||
|
}
|
||||||
|
go dp.workerLoop()
|
||||||
|
return dp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func stageNativePluginDLL(manifest PluginManifest, data []byte) (string, error) {
|
||||||
|
sum := sha256.Sum256(data)
|
||||||
|
hash := hex.EncodeToString(sum[:])
|
||||||
|
root, err := os.UserCacheDir()
|
||||||
|
if err != nil || strings.TrimSpace(root) == "" {
|
||||||
|
root = os.TempDir()
|
||||||
|
}
|
||||||
|
pluginID := sanitizeCacheName(manifest.ID)
|
||||||
|
if pluginID == "" {
|
||||||
|
pluginID = "plugin"
|
||||||
|
}
|
||||||
|
dir := filepath.Join(root, "Overlord", "plugins", pluginID, hash)
|
||||||
|
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||||
|
return "", fmt.Errorf("create plugin cache: %w", err)
|
||||||
|
}
|
||||||
|
target := filepath.Join(dir, pluginID+".dll")
|
||||||
|
if st, err := os.Stat(target); err == nil && st.Size() == int64(len(data)) {
|
||||||
|
return target, nil
|
||||||
|
}
|
||||||
|
tmp := target + ".tmp"
|
||||||
|
if err := os.WriteFile(tmp, data, 0600); err != nil {
|
||||||
|
return "", fmt.Errorf("write plugin cache: %w", err)
|
||||||
|
}
|
||||||
|
if err := os.Rename(tmp, target); err != nil {
|
||||||
|
_ = os.Remove(tmp)
|
||||||
|
if st, statErr := os.Stat(target); statErr == nil && st.Size() == int64(len(data)) {
|
||||||
|
return target, nil
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("finalize plugin cache: %w", err)
|
||||||
|
}
|
||||||
|
return target, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitizeCacheName(value string) string {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
var b strings.Builder
|
||||||
|
for _, r := range value {
|
||||||
|
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' || r == '_' || r == '.' {
|
||||||
|
b.WriteRune(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
type pluginWork struct {
|
type pluginWork struct {
|
||||||
fn func() error
|
fn func() error
|
||||||
done chan error
|
done chan error
|
||||||
@@ -115,6 +265,8 @@ type pluginWork struct {
|
|||||||
|
|
||||||
type dllPlugin struct {
|
type dllPlugin struct {
|
||||||
mem *MemoryModule
|
mem *MemoryModule
|
||||||
|
module windows.Handle
|
||||||
|
modulePath string
|
||||||
onLoadAddr uintptr
|
onLoadAddr uintptr
|
||||||
onEventAddr uintptr
|
onEventAddr uintptr
|
||||||
onUnloadAddr uintptr
|
onUnloadAddr uintptr
|
||||||
@@ -181,23 +333,24 @@ func (p *dllPlugin) Event(event string, payload []byte) error {
|
|||||||
log.Printf("[panic] plugin event panic: %v", r)
|
log.Printf("[panic] plugin event panic: %v", r)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
runtime.LockOSThread()
|
if err := p.runOnWorker(func() error {
|
||||||
defer runtime.UnlockOSThread()
|
var eventPtr, payloadPtr uintptr
|
||||||
|
eventLen := uintptr(len(eventBytes))
|
||||||
cleanup := p.mem.SetupThreadTLS()
|
payloadLen := uintptr(len(payloadCopy))
|
||||||
defer cleanup()
|
if len(eventBytes) > 0 {
|
||||||
|
eventPtr = uintptr(unsafe.Pointer(&eventBytes[0]))
|
||||||
var eventPtr, payloadPtr uintptr
|
}
|
||||||
eventLen := uintptr(len(eventBytes))
|
if len(payloadCopy) > 0 {
|
||||||
payloadLen := uintptr(len(payloadCopy))
|
payloadPtr = uintptr(unsafe.Pointer(&payloadCopy[0]))
|
||||||
if len(eventBytes) > 0 {
|
}
|
||||||
eventPtr = uintptr(unsafe.Pointer(&eventBytes[0]))
|
ret, _, _ := syscall.SyscallN(p.onEventAddr, eventPtr, eventLen, payloadPtr, payloadLen)
|
||||||
|
if int32(ret) != 0 {
|
||||||
|
return errors.New("PluginOnEvent returned non-zero")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
log.Printf("[plugin] event error: %v", err)
|
||||||
}
|
}
|
||||||
if len(payloadCopy) > 0 {
|
|
||||||
payloadPtr = uintptr(unsafe.Pointer(&payloadCopy[0]))
|
|
||||||
}
|
|
||||||
|
|
||||||
syscall.SyscallN(p.onEventAddr, eventPtr, eventLen, payloadPtr, payloadLen)
|
|
||||||
}()
|
}()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -219,8 +372,13 @@ func (p *dllPlugin) Close() error {
|
|||||||
p.mem.Free()
|
p.mem.Free()
|
||||||
p.mem = nil
|
p.mem = nil
|
||||||
}
|
}
|
||||||
|
if p.module != 0 {
|
||||||
|
_ = windows.FreeLibrary(p.module)
|
||||||
|
p.module = 0
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
p.mem = nil
|
p.mem = nil
|
||||||
|
p.module = 0
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ func (m *Manager) Load(ctx context.Context, manifest PluginManifest, binary []by
|
|||||||
if manifest.RuntimeKind == "wasm" || manifest.WASM != "" {
|
if manifest.RuntimeKind == "wasm" || manifest.WASM != "" {
|
||||||
np, err = loadWASMPlugin(ctx, manifest, binary)
|
np, err = loadWASMPlugin(ctx, manifest, binary)
|
||||||
} else {
|
} else {
|
||||||
np, err = loadNativePlugin(binary)
|
np, err = loadNativePlugin(manifest, binary)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package plugins
|
||||||
|
|
||||||
|
type nativeEntryNames struct {
|
||||||
|
onLoad string
|
||||||
|
onEvent string
|
||||||
|
onUnload string
|
||||||
|
setCallback string
|
||||||
|
getRuntime string
|
||||||
|
}
|
||||||
|
|
||||||
|
func nativeEntries(manifest PluginManifest) nativeEntryNames {
|
||||||
|
names := nativeEntryNames{
|
||||||
|
onLoad: "PluginOnLoad",
|
||||||
|
onEvent: "PluginOnEvent",
|
||||||
|
onUnload: "PluginOnUnload",
|
||||||
|
setCallback: "PluginSetCallback",
|
||||||
|
getRuntime: "PluginGetRuntime",
|
||||||
|
}
|
||||||
|
if manifest.NativeEntrypoints.OnLoad != "" {
|
||||||
|
names.onLoad = manifest.NativeEntrypoints.OnLoad
|
||||||
|
}
|
||||||
|
if manifest.NativeEntrypoints.OnEvent != "" {
|
||||||
|
names.onEvent = manifest.NativeEntrypoints.OnEvent
|
||||||
|
}
|
||||||
|
if manifest.NativeEntrypoints.OnUnload != "" {
|
||||||
|
names.onUnload = manifest.NativeEntrypoints.OnUnload
|
||||||
|
}
|
||||||
|
if manifest.NativeEntrypoints.SetCallback != "" {
|
||||||
|
names.setCallback = manifest.NativeEntrypoints.SetCallback
|
||||||
|
}
|
||||||
|
if manifest.NativeEntrypoints.GetRuntime != "" {
|
||||||
|
names.getRuntime = manifest.NativeEntrypoints.GetRuntime
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ const (
|
|||||||
|
|
||||||
imageDirectoryEntryExport = 0
|
imageDirectoryEntryExport = 0
|
||||||
imageDirectoryEntryImport = 1
|
imageDirectoryEntryImport = 1
|
||||||
|
imageDirectoryEntryException = 3
|
||||||
imageDirectoryEntryBaseReloc = 5
|
imageDirectoryEntryBaseReloc = 5
|
||||||
imageDirectoryEntryTLS = 9
|
imageDirectoryEntryTLS = 9
|
||||||
|
|
||||||
@@ -122,17 +123,26 @@ type imageTLSDirectory64 struct {
|
|||||||
Characteristics uint32
|
Characteristics uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type imageRuntimeFunction struct {
|
||||||
|
BeginAddress uint32
|
||||||
|
EndAddress uint32
|
||||||
|
UnwindInfoAddress uint32
|
||||||
|
}
|
||||||
|
|
||||||
type MemoryModule struct {
|
type MemoryModule struct {
|
||||||
base uintptr
|
base uintptr
|
||||||
size uintptr
|
size uintptr
|
||||||
entryPoint uintptr
|
entryPoint uintptr
|
||||||
exports map[string]uintptr
|
exports map[string]uintptr
|
||||||
importDLLs []windows.Handle
|
importDLLs []windows.Handle
|
||||||
initialized bool
|
initialized bool
|
||||||
tlsIndex uint32 // index returned by TlsAlloc; 0xFFFFFFFF if no TLS dir
|
functionTable uintptr
|
||||||
tlsTemplSrc uintptr // PE template raw data start (for copying to new threads)
|
functionCount uint32
|
||||||
tlsTemplSize uintptr // dataSize (template bytes to copy)
|
functionTableRegistered bool
|
||||||
tlsTotalSize uintptr // dataSize + zeroFill (total allocation per thread)
|
tlsIndex uint32 // index returned by TlsAlloc; 0xFFFFFFFF if no TLS dir
|
||||||
|
tlsTemplSrc uintptr // PE template raw data start (for copying to new threads)
|
||||||
|
tlsTemplSize uintptr // dataSize (template bytes to copy)
|
||||||
|
tlsTotalSize uintptr // dataSize + zeroFill (total allocation per thread)
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadMemoryModule(data []byte) (*MemoryModule, error) {
|
func LoadMemoryModule(data []byte) (*MemoryModule, error) {
|
||||||
@@ -251,6 +261,11 @@ func LoadMemoryModule(data []byte) (*MemoryModule, error) {
|
|||||||
_ = windows.VirtualProtect(base+uintptr(sec.VirtualAddress), size, prot, &oldProt)
|
_ = windows.VirtualProtect(base+uintptr(sec.VirtualAddress), size, prot, &oldProt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := mm.registerExceptionTable(); err != nil {
|
||||||
|
mm.Free()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
mm.setupTLS(dllProcessAttach)
|
mm.setupTLS(dllProcessAttach)
|
||||||
|
|
||||||
if mappedNT.OptionalHeader.AddressOfEntryPoint != 0 {
|
if mappedNT.OptionalHeader.AddressOfEntryPoint != 0 {
|
||||||
@@ -293,6 +308,10 @@ func (mm *MemoryModule) Free() {
|
|||||||
syscall.SyscallN(mm.entryPoint, mm.base, dllProcessDetach, 0)
|
syscall.SyscallN(mm.entryPoint, mm.base, dllProcessDetach, 0)
|
||||||
mm.initialized = false
|
mm.initialized = false
|
||||||
}
|
}
|
||||||
|
if mm.functionTableRegistered {
|
||||||
|
_, _, _ = procRtlDeleteFunctionTable.Call(mm.functionTable)
|
||||||
|
mm.functionTableRegistered = false
|
||||||
|
}
|
||||||
if mm.tlsIndex != tlsOutOfIndexes {
|
if mm.tlsIndex != tlsOutOfIndexes {
|
||||||
// Free our load-thread per-thread block (held in the slot)
|
// Free our load-thread per-thread block (held in the slot)
|
||||||
// before releasing the slot itself, so the OS can reuse it.
|
// before releasing the slot itself, so the OS can reuse it.
|
||||||
@@ -346,6 +365,32 @@ func (mm *MemoryModule) processRelocations(dir imageDataDirectory, delta int64)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (mm *MemoryModule) registerExceptionTable() error {
|
||||||
|
dosHdr := (*imageDOSHeader)(unsafe.Pointer(mm.base))
|
||||||
|
ntHdr := (*imageNTHeaders64)(unsafe.Pointer(mm.base + uintptr(dosHdr.LfaNew)))
|
||||||
|
excDir := ntHdr.OptionalHeader.DataDirectory[imageDirectoryEntryException]
|
||||||
|
if excDir.VirtualAddress == 0 || excDir.Size == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
entrySize := uint32(unsafe.Sizeof(imageRuntimeFunction{}))
|
||||||
|
if excDir.Size%entrySize != 0 {
|
||||||
|
return fmt.Errorf("pe: invalid exception directory size")
|
||||||
|
}
|
||||||
|
if uintptr(excDir.VirtualAddress)+uintptr(excDir.Size) > mm.size {
|
||||||
|
return fmt.Errorf("pe: exception directory out of bounds")
|
||||||
|
}
|
||||||
|
table := mm.base + uintptr(excDir.VirtualAddress)
|
||||||
|
count := excDir.Size / entrySize
|
||||||
|
r, _, err := procRtlAddFunctionTable.Call(table, uintptr(count), mm.base)
|
||||||
|
if r == 0 {
|
||||||
|
return fmt.Errorf("pe: RtlAddFunctionTable failed: %w", err)
|
||||||
|
}
|
||||||
|
mm.functionTable = table
|
||||||
|
mm.functionCount = count
|
||||||
|
mm.functionTableRegistered = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (mm *MemoryModule) resolveImports(dir imageDataDirectory) error {
|
func (mm *MemoryModule) resolveImports(dir imageDataDirectory) error {
|
||||||
descSize := unsafe.Sizeof(imageImportDescriptor{})
|
descSize := unsafe.Sizeof(imageImportDescriptor{})
|
||||||
offset := uintptr(dir.VirtualAddress)
|
offset := uintptr(dir.VirtualAddress)
|
||||||
@@ -448,8 +493,14 @@ func (mm *MemoryModule) setupTLS(reason uint32) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tls := (*imageTLSDirectory64)(unsafe.Pointer(mm.base + uintptr(tlsDir.VirtualAddress)))
|
tls := (*imageTLSDirectory64)(unsafe.Pointer(mm.base + uintptr(tlsDir.VirtualAddress)))
|
||||||
|
if tls.EndAddressOfRawData < tls.StartAddressOfRawData {
|
||||||
|
return
|
||||||
|
}
|
||||||
dataSize := uintptr(tls.EndAddressOfRawData - tls.StartAddressOfRawData)
|
dataSize := uintptr(tls.EndAddressOfRawData - tls.StartAddressOfRawData)
|
||||||
totalSize := dataSize + uintptr(tls.SizeOfZeroFill)
|
totalSize := dataSize + uintptr(tls.SizeOfZeroFill)
|
||||||
|
if dataSize > 0 && !mm.containsAddress(uintptr(tls.StartAddressOfRawData), dataSize) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Reserve a real TLS slot through the Win32 loader. ntdll grows the
|
// Reserve a real TLS slot through the Win32 loader. ntdll grows the
|
||||||
// TEB's TLS array to fit, and this slot is interoperable with the
|
// TEB's TLS array to fit, and this slot is interoperable with the
|
||||||
@@ -494,6 +545,9 @@ func (mm *MemoryModule) setupTLS(reason uint32) {
|
|||||||
|
|
||||||
if tls.AddressOfCallBacks != 0 {
|
if tls.AddressOfCallBacks != 0 {
|
||||||
cbAddr := uintptr(tls.AddressOfCallBacks)
|
cbAddr := uintptr(tls.AddressOfCallBacks)
|
||||||
|
if !mm.containsAddress(cbAddr, unsafe.Sizeof(uintptr(0))) {
|
||||||
|
return
|
||||||
|
}
|
||||||
for {
|
for {
|
||||||
cb := *(*uintptr)(unsafe.Pointer(cbAddr))
|
cb := *(*uintptr)(unsafe.Pointer(cbAddr))
|
||||||
if cb == 0 {
|
if cb == 0 {
|
||||||
@@ -501,6 +555,9 @@ func (mm *MemoryModule) setupTLS(reason uint32) {
|
|||||||
}
|
}
|
||||||
syscall.SyscallN(cb, mm.base, uintptr(reason), 0)
|
syscall.SyscallN(cb, mm.base, uintptr(reason), 0)
|
||||||
cbAddr += 8
|
cbAddr += 8
|
||||||
|
if !mm.containsAddress(cbAddr, unsafe.Sizeof(uintptr(0))) {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -537,6 +594,17 @@ func (mm *MemoryModule) SetupThreadTLS() func() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (mm *MemoryModule) containsAddress(addr uintptr, size uintptr) bool {
|
||||||
|
if size == 0 {
|
||||||
|
return addr >= mm.base && addr <= mm.base+mm.size
|
||||||
|
}
|
||||||
|
if addr < mm.base {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
offset := addr - mm.base
|
||||||
|
return offset <= mm.size && size <= mm.size-offset
|
||||||
|
}
|
||||||
|
|
||||||
func peString(addr uintptr) string {
|
func peString(addr uintptr) string {
|
||||||
var buf []byte
|
var buf []byte
|
||||||
for i := 0; i < 4096; i++ { // safety limit
|
for i := 0; i < 4096; i++ { // safety limit
|
||||||
@@ -577,12 +645,14 @@ func sectionProtection(chars uint32) uint32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
modKernel32 = windows.NewLazySystemDLL("kernel32.dll")
|
modKernel32 = windows.NewLazySystemDLL("kernel32.dll")
|
||||||
procGetProcAddr = modKernel32.NewProc("GetProcAddress")
|
procGetProcAddr = modKernel32.NewProc("GetProcAddress")
|
||||||
procTlsAlloc = modKernel32.NewProc("TlsAlloc")
|
procRtlAddFunctionTable = modKernel32.NewProc("RtlAddFunctionTable")
|
||||||
procTlsFree = modKernel32.NewProc("TlsFree")
|
procRtlDeleteFunctionTable = modKernel32.NewProc("RtlDeleteFunctionTable")
|
||||||
procTlsSetValue = modKernel32.NewProc("TlsSetValue")
|
procTlsAlloc = modKernel32.NewProc("TlsAlloc")
|
||||||
procTlsGetValue = modKernel32.NewProc("TlsGetValue")
|
procTlsFree = modKernel32.NewProc("TlsFree")
|
||||||
|
procTlsSetValue = modKernel32.NewProc("TlsSetValue")
|
||||||
|
procTlsGetValue = modKernel32.NewProc("TlsGetValue")
|
||||||
)
|
)
|
||||||
|
|
||||||
const tlsOutOfIndexes uint32 = 0xFFFFFFFF
|
const tlsOutOfIndexes uint32 = 0xFFFFFFFF
|
||||||
|
|||||||
@@ -9,24 +9,34 @@ type PluginAssets struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PluginManifest struct {
|
type PluginManifest struct {
|
||||||
ID string `msgpack:"id" json:"id"`
|
ID string `msgpack:"id" json:"id"`
|
||||||
Name string `msgpack:"name" json:"name"`
|
Name string `msgpack:"name" json:"name"`
|
||||||
APIVersion int `msgpack:"apiVersion,omitempty" json:"apiVersion,omitempty"`
|
APIVersion int `msgpack:"apiVersion,omitempty" json:"apiVersion,omitempty"`
|
||||||
RuntimeKind string `msgpack:"runtime,omitempty" json:"runtime,omitempty"`
|
RuntimeKind string `msgpack:"runtime,omitempty" json:"runtime,omitempty"`
|
||||||
Version string `msgpack:"version,omitempty" json:"version,omitempty"`
|
NativeLoader string `msgpack:"nativeLoader,omitempty" json:"nativeLoader,omitempty"`
|
||||||
Description string `msgpack:"description,omitempty" json:"description,omitempty"`
|
NativeEntrypoints NativeEntrypoints `msgpack:"nativeEntrypoints,omitempty" json:"nativeEntrypoints,omitempty"`
|
||||||
Binary string `msgpack:"binary,omitempty" json:"binary,omitempty"`
|
Version string `msgpack:"version,omitempty" json:"version,omitempty"`
|
||||||
Binaries map[string]string `msgpack:"binaries,omitempty" json:"binaries,omitempty"`
|
Description string `msgpack:"description,omitempty" json:"description,omitempty"`
|
||||||
WASM string `msgpack:"wasm,omitempty" json:"wasm,omitempty"`
|
Binary string `msgpack:"binary,omitempty" json:"binary,omitempty"`
|
||||||
Needs PluginNeeds `msgpack:"needs,omitempty" json:"needs,omitempty"`
|
Binaries map[string]string `msgpack:"binaries,omitempty" json:"binaries,omitempty"`
|
||||||
Entry string `msgpack:"entry,omitempty" json:"entry,omitempty"`
|
WASM string `msgpack:"wasm,omitempty" json:"wasm,omitempty"`
|
||||||
Assets PluginAssets `msgpack:"assets,omitempty" json:"assets,omitempty"`
|
Needs PluginNeeds `msgpack:"needs,omitempty" json:"needs,omitempty"`
|
||||||
|
Entry string `msgpack:"entry,omitempty" json:"entry,omitempty"`
|
||||||
|
Assets PluginAssets `msgpack:"assets,omitempty" json:"assets,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PluginNeeds struct {
|
type PluginNeeds struct {
|
||||||
Files []PluginFileNeed `msgpack:"files,omitempty" json:"files,omitempty"`
|
Files []PluginFileNeed `msgpack:"files,omitempty" json:"files,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NativeEntrypoints struct {
|
||||||
|
OnLoad string `msgpack:"onLoad,omitempty" json:"onLoad,omitempty"`
|
||||||
|
OnEvent string `msgpack:"onEvent,omitempty" json:"onEvent,omitempty"`
|
||||||
|
OnUnload string `msgpack:"onUnload,omitempty" json:"onUnload,omitempty"`
|
||||||
|
SetCallback string `msgpack:"setCallback,omitempty" json:"setCallback,omitempty"`
|
||||||
|
GetRuntime string `msgpack:"getRuntime,omitempty" json:"getRuntime,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type PluginFileNeed struct {
|
type PluginFileNeed struct {
|
||||||
Bucket string `msgpack:"bucket" json:"bucket"`
|
Bucket string `msgpack:"bucket" json:"bucket"`
|
||||||
Access []string `msgpack:"access" json:"access"`
|
Access []string `msgpack:"access" json:"access"`
|
||||||
@@ -67,11 +77,21 @@ func ManifestFromMap(m map[string]interface{}) (PluginManifest, error) {
|
|||||||
manifest.Name = stringVal(m["name"])
|
manifest.Name = stringVal(m["name"])
|
||||||
manifest.APIVersion = intVal(m["apiVersion"])
|
manifest.APIVersion = intVal(m["apiVersion"])
|
||||||
manifest.RuntimeKind = stringVal(m["runtime"])
|
manifest.RuntimeKind = stringVal(m["runtime"])
|
||||||
|
manifest.NativeLoader = stringVal(m["nativeLoader"])
|
||||||
manifest.Version = stringVal(m["version"])
|
manifest.Version = stringVal(m["version"])
|
||||||
manifest.Description = stringVal(m["description"])
|
manifest.Description = stringVal(m["description"])
|
||||||
manifest.Binary = stringVal(m["binary"])
|
manifest.Binary = stringVal(m["binary"])
|
||||||
manifest.WASM = stringVal(m["wasm"])
|
manifest.WASM = stringVal(m["wasm"])
|
||||||
manifest.Entry = stringVal(m["entry"])
|
manifest.Entry = stringVal(m["entry"])
|
||||||
|
if entryRaw, ok := m["nativeEntrypoints"].(map[string]interface{}); ok {
|
||||||
|
manifest.NativeEntrypoints = NativeEntrypoints{
|
||||||
|
OnLoad: stringVal(entryRaw["onLoad"]),
|
||||||
|
OnEvent: stringVal(entryRaw["onEvent"]),
|
||||||
|
OnUnload: stringVal(entryRaw["onUnload"]),
|
||||||
|
SetCallback: stringVal(entryRaw["setCallback"]),
|
||||||
|
GetRuntime: stringVal(entryRaw["getRuntime"]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if binariesRaw, ok := m["binaries"].(map[string]interface{}); ok {
|
if binariesRaw, ok := m["binaries"].(map[string]interface{}); ok {
|
||||||
manifest.Binaries = make(map[string]string, len(binariesRaw))
|
manifest.Binaries = make(map[string]string, len(binariesRaw))
|
||||||
|
|||||||
@@ -357,6 +357,8 @@ export type PluginManifest = {
|
|||||||
name: string;
|
name: string;
|
||||||
apiVersion?: number;
|
apiVersion?: number;
|
||||||
runtime?: "native" | "wasm" | "server" | string;
|
runtime?: "native" | "wasm" | "server" | string;
|
||||||
|
nativeLoader?: "memory" | "os" | string;
|
||||||
|
nativeEntrypoints?: PluginNativeEntrypoints;
|
||||||
version?: string;
|
version?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
binary?: string;
|
binary?: string;
|
||||||
@@ -378,6 +380,14 @@ export type PluginManifest = {
|
|||||||
hasServer?: boolean;
|
hasServer?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PluginNativeEntrypoints = {
|
||||||
|
onLoad?: string;
|
||||||
|
onEvent?: string;
|
||||||
|
onUnload?: string;
|
||||||
|
setCallback?: string;
|
||||||
|
getRuntime?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type PluginDashboardIntegration = {
|
export type PluginDashboardIntegration = {
|
||||||
clientBadges?: PluginDashboardBadgeDefinition[];
|
clientBadges?: PluginDashboardBadgeDefinition[];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -96,6 +96,40 @@ describe("loadPluginBundle", () => {
|
|||||||
expect(bundle.binaryPath).toBe(join(root, "plink", "plink-windows-amd64.dll"));
|
expect(bundle.binaryPath).toBe(join(root, "plink", "plink-windows-amd64.dll"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("preserves native loader and custom entrypoint metadata from config", async () => {
|
||||||
|
const root = await createTempRoot();
|
||||||
|
const AdmZip = require("adm-zip");
|
||||||
|
const zip = new AdmZip();
|
||||||
|
zip.addFile("config.json", Buffer.from(JSON.stringify({
|
||||||
|
name: "Native Loader Demo",
|
||||||
|
apiVersion: 2,
|
||||||
|
runtime: "native",
|
||||||
|
nativeLoader: "loadlibraryex",
|
||||||
|
nativeEntrypoints: {
|
||||||
|
onLoad: "StartPlugin",
|
||||||
|
onEvent: "HandlePluginEvent",
|
||||||
|
onUnload: "StopPlugin",
|
||||||
|
setCallback: "SetHostCallback",
|
||||||
|
getRuntime: "RuntimeName",
|
||||||
|
},
|
||||||
|
})));
|
||||||
|
zip.addFile("native-loader-demo.html", Buffer.from("<div id=\"native-loader-demo\"></div>"));
|
||||||
|
zip.addFile("native-loader-demo.css", Buffer.from("#native-loader-demo { display: block; }"));
|
||||||
|
zip.addFile("native-loader-demo.js", Buffer.from("document.body.dataset.nativeLoaderDemo = '1';"));
|
||||||
|
zip.addFile("native-loader-demo-windows-amd64.dll", Buffer.from("native-dll"));
|
||||||
|
zip.writeZip(join(root, "native-loader-demo.zip"));
|
||||||
|
|
||||||
|
await ensurePluginExtracted(root, "native-loader-demo", (name) => name);
|
||||||
|
const manifest = JSON.parse(await readFile(join(root, "native-loader-demo", "manifest.json"), "utf-8"));
|
||||||
|
|
||||||
|
expect(manifest.nativeLoader).toBe("os");
|
||||||
|
expect(manifest.nativeEntrypoints.onLoad).toBe("StartPlugin");
|
||||||
|
expect(manifest.nativeEntrypoints.onEvent).toBe("HandlePluginEvent");
|
||||||
|
expect(manifest.nativeEntrypoints.onUnload).toBe("StopPlugin");
|
||||||
|
expect(manifest.nativeEntrypoints.setCallback).toBe("SetHostCallback");
|
||||||
|
expect(manifest.nativeEntrypoints.getRuntime).toBe("RuntimeName");
|
||||||
|
});
|
||||||
|
|
||||||
test("preserves dashboard badge metadata from config", async () => {
|
test("preserves dashboard badge metadata from config", async () => {
|
||||||
const root = await createTempRoot();
|
const root = await createTempRoot();
|
||||||
const AdmZip = require("adm-zip");
|
const AdmZip = require("adm-zip");
|
||||||
|
|||||||
@@ -200,6 +200,8 @@ export async function ensurePluginExtracted(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const runtime = normalizePluginRuntime(extraConfig.runtime, wasmEntry !== null);
|
const runtime = normalizePluginRuntime(extraConfig.runtime, wasmEntry !== null);
|
||||||
|
const nativeLoader = normalizeNativeLoader(extraConfig.nativeLoader);
|
||||||
|
const nativeEntrypoints = normalizeNativeEntrypoints(extraConfig.nativeEntrypoints);
|
||||||
const needs = normalizePluginNeeds(extraConfig.needs);
|
const needs = normalizePluginNeeds(extraConfig.needs);
|
||||||
const build = normalizePluginBuildIntegration(extraConfig.build);
|
const build = normalizePluginBuildIntegration(extraConfig.build);
|
||||||
const manifest: PluginManifest = {
|
const manifest: PluginManifest = {
|
||||||
@@ -207,6 +209,8 @@ export async function ensurePluginExtracted(
|
|||||||
name: extraConfig.name || safeId,
|
name: extraConfig.name || safeId,
|
||||||
apiVersion: Number(extraConfig.apiVersion) === 2 || runtime === "wasm" ? 2 : 1,
|
apiVersion: Number(extraConfig.apiVersion) === 2 || runtime === "wasm" ? 2 : 1,
|
||||||
runtime,
|
runtime,
|
||||||
|
...(nativeLoader && { nativeLoader }),
|
||||||
|
...(nativeEntrypoints && { nativeEntrypoints }),
|
||||||
version: extraConfig.version || "1.0.0",
|
version: extraConfig.version || "1.0.0",
|
||||||
description: extraConfig.description,
|
description: extraConfig.description,
|
||||||
...(typeof extraConfig.binary === "string" && extraConfig.binary && { binary: extraConfig.binary }),
|
...(typeof extraConfig.binary === "string" && extraConfig.binary && { binary: extraConfig.binary }),
|
||||||
@@ -634,6 +638,26 @@ function normalizePluginRuntime(runtime: any, hasWasm: boolean): "native" | "was
|
|||||||
return "native";
|
return "native";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeNativeLoader(loader: any): "memory" | "os" | undefined {
|
||||||
|
const value = typeof loader === "string" ? loader.trim().toLowerCase() : "";
|
||||||
|
if (["memory", "mem", "reflective", "manual"].includes(value)) return "memory";
|
||||||
|
if (["os", "disk", "file", "loadlibrary", "loadlibraryex"].includes(value)) return "os";
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeNativeEntrypoints(raw: any): any | undefined {
|
||||||
|
if (!raw || typeof raw !== "object") return undefined;
|
||||||
|
const out: any = {};
|
||||||
|
for (const key of ["onLoad", "onEvent", "onUnload", "setCallback", "getRuntime"]) {
|
||||||
|
const value = raw[key];
|
||||||
|
if (typeof value !== "string") continue;
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!/^[A-Za-z_][A-Za-z0-9_@$?.-]{0,127}$/.test(trimmed)) continue;
|
||||||
|
out[key] = trimmed;
|
||||||
|
}
|
||||||
|
return Object.keys(out).length > 0 ? out : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
function normalizePluginBuildIntegration(raw: any): any | undefined {
|
function normalizePluginBuildIntegration(raw: any): any | undefined {
|
||||||
if (!raw || typeof raw !== "object") return undefined;
|
if (!raw || typeof raw !== "object") return undefined;
|
||||||
const build: any = {};
|
const build: any = {};
|
||||||
|
|||||||
@@ -40,6 +40,24 @@ Use:
|
|||||||
|
|
||||||
Server-only plugins are not sent to clients and do not use auto-load.
|
Server-only plugins are not sent to clients and do not use auto-load.
|
||||||
|
|
||||||
|
Native plugins can choose the Windows loader mode and exported ABI names:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"runtime": "native",
|
||||||
|
"nativeLoader": "os",
|
||||||
|
"nativeEntrypoints": {
|
||||||
|
"onLoad": "MyLoad",
|
||||||
|
"onEvent": "MyEvent",
|
||||||
|
"onUnload": "MyUnload",
|
||||||
|
"setCallback": "MySetCallback",
|
||||||
|
"getRuntime": "MyRuntime"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`nativeLoader` accepts `memory` for the in-memory PE loader or `os` for the platform loader. On Windows, `os` stages the DLL in the agent cache and loads it with `LoadLibraryExW`, which is more compatible with runtimes that expect normal loader initialization. Omit `nativeEntrypoints` to use the standard `PluginOnLoad`, `PluginOnEvent`, `PluginOnUnload`, `PluginSetCallback`, and `PluginGetRuntime` exports.
|
||||||
|
|
||||||
## Manifest Fields
|
## Manifest Fields
|
||||||
|
|
||||||
`config.json` is merged into the generated `manifest.json`.
|
`config.json` is merged into the generated `manifest.json`.
|
||||||
|
|||||||
@@ -172,12 +172,49 @@ Go shared libraries cannot be fully unloaded safely because the Go runtime owns
|
|||||||
|
|
||||||
| Platform | Loading behavior |
|
| Platform | Loading behavior |
|
||||||
|----------|------------------|
|
|----------|------------------|
|
||||||
| Windows | DLLs are loaded from memory through the custom PE loader. |
|
| Windows | DLLs default to the custom in-memory PE loader. Plugins can set `nativeLoader: "os"` to stage the DLL in the agent cache and load it with `LoadLibraryExW`. |
|
||||||
| Linux | Shared libraries are loaded through the static-agent subprocess shim when needed. |
|
| Linux | Shared libraries are loaded through the static-agent subprocess shim when needed. |
|
||||||
| macOS | Dynamic libraries are loaded through the platform loader. |
|
| macOS | Dynamic libraries are loaded through the platform loader. |
|
||||||
|
|
||||||
Because Windows uses an in-memory PE loader, some runtime primitives that assume normal `LoadLibrary` initialization can be fragile. The Rust sample avoids `std::sync::Mutex` for this reason and uses C-style globals; the host serializes plugin calls.
|
Because Windows uses an in-memory PE loader, some runtime primitives that assume normal `LoadLibrary` initialization can be fragile. The Rust sample avoids `std::sync::Mutex` for this reason and uses C-style globals; the host serializes plugin calls.
|
||||||
|
|
||||||
|
### Windows Loader Selection
|
||||||
|
|
||||||
|
Set `nativeLoader` in `config.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"runtime": "native",
|
||||||
|
"nativeLoader": "memory"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Value | Behavior |
|
||||||
|
|-------|----------|
|
||||||
|
| `memory` | Default. Loads the PE image from bytes without staging the plugin DLL on disk. |
|
||||||
|
| `os` | Writes a content-addressed copy to the agent cache and loads it with `LoadLibraryExW`. Prefer this for Go DLLs or other runtimes that rely on the normal Windows loader. |
|
||||||
|
|
||||||
|
The OS loader path reuses the cached DLL when the SHA-256 hash matches, so repeated loads do not rewrite the same plugin. Windows keeps loaded images locked; old cached plugin versions are left for later cleanup.
|
||||||
|
|
||||||
|
### Custom Export Names
|
||||||
|
|
||||||
|
Native plugins may override the exported ABI names in `config.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"runtime": "native",
|
||||||
|
"nativeEntrypoints": {
|
||||||
|
"onLoad": "StartPlugin",
|
||||||
|
"onEvent": "HandlePluginEvent",
|
||||||
|
"onUnload": "StopPlugin",
|
||||||
|
"setCallback": "SetHostCallback",
|
||||||
|
"getRuntime": "RuntimeName"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Omitted fields use the standard names. The signatures must stay ABI-compatible with `PluginOnLoad`, `PluginOnEvent`, `PluginOnUnload`, `PluginSetCallback`, and `PluginGetRuntime`; only the exported symbol names change. Custom names are honored by the Windows loader and direct `dlopen`/`dlsym` paths. The Linux static-agent subprocess shim still expects the standard names.
|
||||||
|
|
||||||
## Static Linux Agent Shim
|
## Static Linux Agent Shim
|
||||||
|
|
||||||
Linux agents are compiled as static musl binaries for portability. Static binaries cannot directly call `dlopen`, so Linux native plugin support uses an embedded plugin-host shim.
|
Linux agents are compiled as static musl binaries for portability. Static binaries cannot directly call `dlopen`, so Linux native plugin support uses an embedded plugin-host shim.
|
||||||
|
|||||||
Reference in New Issue
Block a user