Files
overlord/Overlord-Client/cmd/agent/plugins/loader_linux_subproc.go
T

325 lines
7.4 KiB
Go
Raw Normal View History

2026-05-13 23:55:51 -04:00
//go:build linux && cgo
package plugins
/*
#include <sys/socket.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <stdint.h>
#ifndef MFD_CLOEXEC
#define MFD_CLOEXEC 0x0001U
#endif
static int make_socketpair(int fds[2]) {
return socketpair(AF_UNIX, SOCK_STREAM, 0, fds);
}
static int sp_memfd_create(void) {
return (int)syscall(SYS_memfd_create, "plugin", MFD_CLOEXEC);
}
// No CLOEXEC — the fd must survive fexecve/exec into the shim process.
static int sp_memfd_create_nocloe(void) {
return (int)syscall(SYS_memfd_create, "ph", 0);
}
static int sp_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;
}
*/
import "C"
import (
"encoding/binary"
"errors"
"fmt"
"io"
"os"
"strconv"
"sync"
"syscall"
"unsafe"
)
// Message type constants — must match plugin_host.c
const (
phMsgLoad = 0x01
phMsgEvent = 0x02
phMsgUnload = 0x03
phMsgCallback = 0x10
phMsgReady = 0x11
phMsgErr = 0x12
phMsgLoadResult = 0x13
)
// loadNativePluginSubproc attempts to load the plugin by fork+exec-ing the
// embedded plugin_host shim. Returns an error if pluginHostBinary is empty
// (not compiled) or if any setup step fails.
func loadNativePluginSubproc(soData []byte) (NativePlugin, error) {
if len(pluginHostBinary) == 0 {
return nil, errors.New("plugin host shim not available for this architecture")
}
// Write the .so to a memfd so the shim can dlopen it.
soFd := int(C.sp_memfd_create())
if soFd < 0 {
return nil, errors.New("memfd_create failed for plugin .so")
}
if C.sp_write_all(C.int(soFd), unsafe.Pointer(&soData[0]), C.size_t(len(soData))) != 0 {
syscall.Close(soFd)
return nil, errors.New("write to plugin .so memfd failed")
}
// Write the shim binary to a memfd WITHOUT close-on-exec so it survives exec.
shimFdC := C.sp_memfd_create_nocloe()
if shimFdC < 0 {
syscall.Close(soFd)
return nil, errors.New("memfd_create for shim failed")
}
shimFile := os.NewFile(uintptr(shimFdC), "plugin_host_shim")
if _, err := shimFile.Write(pluginHostBinary); err != nil {
shimFile.Close()
syscall.Close(soFd)
return nil, fmt.Errorf("write shim to memfd: %w", err)
}
// Create a socketpair for bidirectional communication.
var fds [2]C.int
if C.make_socketpair(&fds[0]) != 0 {
shimFile.Close()
syscall.Close(soFd)
return nil, errors.New("socketpair failed")
}
parentSock := int(fds[0])
childSock := int(fds[1])
// ForkExec the shim. Pass fds to the child via ProcAttr.Files:
// child fd 0 = /dev/null
// child fd 1 = parent stdout (for debug output)
// child fd 2 = parent stderr
// child fd 3 = .so memfd
// child fd 4 = child end of socketpair
devNull, _ := syscall.Open("/dev/null", syscall.O_RDONLY, 0)
shimPath := fmt.Sprintf("/proc/self/fd/%d", int(shimFdC))
pid, err := syscall.ForkExec(shimPath, []string{"plugin_host",
strconv.Itoa(3), strconv.Itoa(4)},
&syscall.ProcAttr{
Files: []uintptr{
uintptr(devNull),
1,
2,
uintptr(soFd),
uintptr(childSock),
},
},
)
// Parent no longer needs these fds.
syscall.Close(devNull)
syscall.Close(soFd)
syscall.Close(childSock)
shimFile.Close()
if err != nil {
syscall.Close(parentSock)
return nil, fmt.Errorf("forkexec plugin_host: %w", err)
}
p := &subProcPlugin{
pid: pid,
sock: os.NewFile(uintptr(parentSock), "plugin_sock"),
loadResultCh: make(chan error, 1),
stopCh: make(chan struct{}),
}
go p.readLoop()
// Wait for MSG_READY (or MSG_ERR) to confirm the shim loaded the .so.
select {
case err := <-p.loadResultCh:
if err != nil {
p.sock.Close()
return nil, err
}
case <-p.stopCh:
p.sock.Close()
return nil, errors.New("plugin host exited before sending READY")
}
return p, nil
}
// subProcPlugin implements NativePlugin by routing calls through the shim subprocess.
type subProcPlugin struct {
pid int
sock *os.File
mu sync.Mutex
pluginRuntime string
sendFn func(string, []byte)
loadResultCh chan error
stopCh chan struct{}
stopOnce sync.Once
}
func (p *subProcPlugin) readLoop() {
defer p.stopOnce.Do(func() { close(p.stopCh) })
for {
msgType, payload, err := p.recvMsg()
if err != nil {
return
}
switch msgType {
case phMsgReady:
p.mu.Lock()
p.pluginRuntime = string(payload)
p.mu.Unlock()
// Signal the constructor that the shim is ready.
select {
case p.loadResultCh <- nil:
default:
}
case phMsgErr:
select {
case p.loadResultCh <- fmt.Errorf("plugin_host: %s", payload):
default:
}
return
case phMsgLoadResult:
if len(payload) > 0 && payload[0] != 0 {
select {
case p.loadResultCh <- errors.New("PluginOnLoad returned non-zero"):
default:
}
} else {
select {
case p.loadResultCh <- nil:
default:
}
}
case phMsgCallback:
if len(payload) < 6 {
continue
}
evLen := int(binary.LittleEndian.Uint16(payload[0:2]))
if len(payload) < 2+evLen+4 {
continue
}
plLen := int(binary.LittleEndian.Uint32(payload[2+evLen:]))
event := string(payload[2 : 2+evLen])
var pl []byte
if plLen > 0 && len(payload) >= 2+evLen+4+plLen {
pl = payload[2+evLen+4 : 2+evLen+4+plLen]
}
p.mu.Lock()
fn := p.sendFn
p.mu.Unlock()
if fn != nil {
fn(event, pl)
}
}
}
}
func (p *subProcPlugin) sendMsg(msgType uint8, payload []byte) error {
total := uint32(1 + len(payload))
hdr := make([]byte, 5)
binary.LittleEndian.PutUint32(hdr[:4], total)
hdr[4] = msgType
p.mu.Lock()
defer p.mu.Unlock()
if _, err := p.sock.Write(hdr); err != nil {
return err
}
if len(payload) > 0 {
if _, err := p.sock.Write(payload); err != nil {
return err
}
}
return nil
}
func (p *subProcPlugin) recvMsg() (uint8, []byte, error) {
hdr := make([]byte, 5)
if _, err := io.ReadFull(p.sock, hdr); err != nil {
return 0, nil, err
}
total := binary.LittleEndian.Uint32(hdr[:4])
if total == 0 {
return 0, nil, errors.New("zero-length message")
}
msgType := hdr[4]
payloadLen := total - 1
if payloadLen == 0 {
return msgType, nil, nil
}
payload := make([]byte, payloadLen)
if _, err := io.ReadFull(p.sock, payload); err != nil {
return 0, nil, err
}
return msgType, payload, nil
}
func (p *subProcPlugin) Load(send func(string, []byte), hostInfo []byte) error {
p.mu.Lock()
p.sendFn = send
p.mu.Unlock()
// Send MSG_LOAD; shim will call PluginOnLoad and reply with MSG_LOAD_RESULT.
if err := p.sendMsg(phMsgLoad, hostInfo); err != nil {
return fmt.Errorf("send MSG_LOAD: %w", err)
}
select {
case err := <-p.loadResultCh:
return err
case <-p.stopCh:
return errors.New("plugin host exited during load")
}
}
func (p *subProcPlugin) Event(event string, payload []byte) error {
evBytes := []byte(event)
evLen := len(evBytes)
plLen := len(payload)
msg := make([]byte, 2+evLen+4+plLen)
binary.LittleEndian.PutUint16(msg[0:2], uint16(evLen))
copy(msg[2:], evBytes)
binary.LittleEndian.PutUint32(msg[2+evLen:], uint32(plLen))
if plLen > 0 {
copy(msg[2+evLen+4:], payload)
}
return p.sendMsg(phMsgEvent, msg)
}
func (p *subProcPlugin) Unload() {
_ = p.sendMsg(phMsgUnload, nil)
}
func (p *subProcPlugin) Close() error {
p.Unload()
p.sock.Close()
// Reap the child process.
if p.pid > 0 {
syscall.Wait4(p.pid, nil, 0, nil)
}
return nil
}
func (p *subProcPlugin) Runtime() string {
p.mu.Lock()
defer p.mu.Unlock()
return p.pluginRuntime
}