diff --git a/internal/pluginhost/loader_windows.go b/internal/pluginhost/loader_windows.go index cbae0a7f7..a0bd9f0fa 100644 --- a/internal/pluginhost/loader_windows.go +++ b/internal/pluginhost/loader_windows.go @@ -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 { diff --git a/internal/pluginhost/loader_windows_test.go b/internal/pluginhost/loader_windows_test.go index c3cd3a7ee..06b160f81 100644 --- a/internal/pluginhost/loader_windows_test.go +++ b/internal/pluginhost/loader_windows_test.go @@ -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 {