fix(pluginhost): stabilize Windows plugin response buffer

This commit is contained in:
yueziji
2026-07-24 19:49:38 +08:00
parent 42f36b94e0
commit 520cfa1026
2 changed files with 81 additions and 2 deletions

View File

@@ -269,13 +269,26 @@ func (c *dynamicLibraryClient) Call(ctx context.Context, method string, request
if len(request) > 0 {
requestPtr = uintptr(unsafe.Pointer(&request[0]))
}
var response windowsBuffer
responseMem, errAlloc := windows.LocalAlloc(
windows.LMEM_FIXED|windows.LMEM_ZEROINIT,
uint32(unsafe.Sizeof(windowsBuffer{})),
)
if errAlloc != nil {
return nil, fmt.Errorf("allocate plugin response buffer: %w", errAlloc)
}
if responseMem == 0 {
return nil, fmt.Errorf("allocate plugin response buffer")
}
defer func() {
_, _ = windows.LocalFree(windows.Handle(responseMem))
}()
response := (*windowsBuffer)(unsafe.Pointer(responseMem))
rc, _, _ := syscall.SyscallN(
c.api.call,
uintptr(unsafe.Pointer(methodBytes)),
requestPtr,
uintptr(len(request)),
uintptr(unsafe.Pointer(&response)),
responseMem,
)
var out []byte
if response.ptr != 0 && response.len > 0 {

View File

@@ -3,15 +3,81 @@
package pluginhost
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"os"
"path/filepath"
"strings"
"syscall"
"testing"
"unsafe"
"golang.org/x/sys/windows"
)
var testReentrantHostCallback uintptr
func TestDynamicLibraryClientCallSurvivesReentrantCallbackStackGrowth(t *testing.T) {
testReentrantHostCallback = syscall.NewCallback(testGrowHostCallbackStack)
client := newGuardedPluginClient(&dynamicLibraryClient{api: windowsPluginAPI{
call: syscall.NewCallback(testReentrantPluginCall),
freeBuffer: syscall.NewCallback(testReentrantPluginFree),
}})
t.Cleanup(client.Shutdown)
got, errCall := client.Call(context.Background(), "model.route", []byte(`{}`))
if errCall != nil {
t.Fatalf("Call() error = %v", errCall)
}
want := `{"ok":true,"result":{"Handled":true}}`
if string(got) != want {
t.Fatalf("Call() response = %q, want %q", got, want)
}
}
func testReentrantPluginCall(_, _, _, responsePtr uintptr) uintptr {
if testReentrantHostCallback == 0 || responsePtr == 0 {
return 1
}
_, _, _ = syscall.SyscallN(testReentrantHostCallback)
raw := []byte(`{"ok":true,"result":{"Handled":true}}`)
mem, errAlloc := windows.LocalAlloc(windows.LMEM_FIXED, uint32(len(raw)))
if errAlloc != nil || mem == 0 {
return 1
}
copy(unsafe.Slice((*byte)(unsafe.Pointer(mem)), len(raw)), raw)
response := (*windowsBuffer)(unsafe.Pointer(responsePtr))
response.ptr = mem
response.len = uintptr(len(raw))
return 0
}
func testReentrantPluginFree(ptr, _ uintptr) uintptr {
if ptr != 0 {
_, _ = windows.LocalFree(windows.Handle(ptr))
}
return 0
}
func testGrowHostCallbackStack() uintptr {
return uintptr(testGrowStack(64))
}
//go:noinline
func testGrowStack(depth int) int {
var padding [1024]byte
for index := range padding {
padding[index] = byte(index + depth)
}
if depth == 0 {
return int(padding[0])
}
return testGrowStack(depth-1) + int(padding[depth%len(padding)])
}
func TestShadowPluginDirIsProcessScoped(t *testing.T) {
dir, errDir := shadowPluginDir()
if errDir != nil {