go: syscall: Windows user32 function (SendInput) behaves incorrectly when called within golang environment

What version of Go are you using (go version)?

go version go1.12.4 windows/amd64
OS Name:                   Microsoft Windows 10 Pro
OS Version:                10.0.17763 N/A Build 17763

Does this issue reproduce with the latest release?

Yes

What operating system and processor architecture are you using (go env)?

go env Output
set GOARCH=amd64
set GOBIN=
set GOCACHE=C:\Users\knutz\AppData\Local\go-build
set GOEXE=.exe
set GOFLAGS=
set GOHOSTARCH=amd64
set GOHOSTOS=windows
set GOOS=windows
set GOPATH=C:\Users\knutz\go
set GOPROXY=
set GORACE=
set GOROOT=C:\Go
set GOTMPDIR=
set GOTOOLDIR=C:\Go\pkg\tool\windows_amd64
set GCCGO=gccgo
set CC=gcc
set CXX=g++
set CGO_ENABLED=1
set GOMOD=
set CGO_CFLAGS=-g -O2
set CGO_CPPFLAGS=
set CGO_CXXFLAGS=-g -O2
set CGO_FFLAGS=-g -O2
set CGO_LDFLAGS=-g -O2
set PKG_CONFIG=pkg-config
set GOGCCFLAGS=-m64 -mthreads -fno-caret-diagnostics -Qunused-arguments -fmessag                                                                                                                       e-length=0 -fdebug-prefix-map=C:\Users\knutz\AppData\Local\Temp\go-build67967260                                                                                                                       6=/tmp/go-build -gno-record-gcc-switches

What did you do?

In our application we use ffi to perform some things in native code on Windows. In particular, we use SendInput() on Windows which dispatches mouse events and keyboard events to the OS. When a DLL is loaded by the go runtime, this function does not behave as expected. This is also when using the syscall library directly with user32.dll to call the function using pure go code.

The unexpected behavior seems to be a result of how the runtime is setup in golang. This bug is quite serious because it might have unintended consequences when interacting with the windows runtime. It might be possible that other w32 functions behave differently than expected.

We are using the Windows user32 function SendInput: https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-sendinput

Go version

package main

import (
	"log"
	"syscall"
	"time"
	"unsafe"
)

var (
	user32        = syscall.NewLazyDLL("user32.dll")
	sendInputProc = user32.NewProc("SendInput")
)

func test() {
	type keyboardInput struct {
		wVk         uint16
		wScan       uint16
		dwFlags     uint32
		time        uint32
		dwExtraInfo uint64
	}

	type input struct {
		inputType uint32
		ki        keyboardInput
		padding   uint64
	}

	var i input
	i.inputType = 1 //INPUT_KEYBOARD
	i.ki.wVk = 0x41 // virtual key code for a
	ret, _, err := sendInputProc.Call(
		uintptr(1),
		uintptr(unsafe.Pointer(&i)),
		uintptr(unsafe.Sizeof(i)),
	)
	log.Printf("ret: %v error: %v", ret, err)
}

func main() {
	done := false
	for !done {
		test()
		<-time.After(time.Second)
	}
	return
}

C version

#include <Windows.h>
 
#include <cstdio>
 
void test()
{
  INPUT ip = {};
 
  ip.type = INPUT_KEYBOARD;
  ip.ki.wVk = 0x41; // virtual-key code for the "a" key
 
  UINT ret = SendInput(1, &ip, sizeof(INPUT));
  DWORD err = GetLastError();
 
  std::printf("ret: %i err: %i\n", (int)ret, (int)err);
}
 
int main()
{
    while(true)
    {
      test();
      Sleep(1000);
    }
}

What did you expect to see?

With both binaries:

  1. Run program, this will output a series of ‘a’ every second, as if typed
  2. Every time an ‘a’ is outputted, the program prints how many events were dispatched (the result from SendInput) as well as if there was an error
  3. Hit ctrl-alt-del to activate the secure desktop
  4. Wait for 2-3 seconds
  5. Go back, quit the program and check the output

Expected output (as in the C version): ret: 1 err: 0 ret: 1 err: 0 ret: 1 err: 0 ret: 0 err: 5 ret: 0 err: 5 ret: 0 err: 5 ret: 1 err: 5

ret: 1 means that 1 event was dispatched ret: 0 means that no events were dispatched err: 5 means access denied in windows

When the secure desktop was present, ret is 0 and err is 5. This is as expected. No events were dispatched and an error was generated.

What did you see instead?

In the golang version you get: 2019/04/25 14:20:43 ret: 1 error: The operation completed successfully. 2019/04/25 14:20:44 ret: 1 error: The operation completed successfully. 2019/04/25 14:20:45 ret: 1 error: The operation completed successfully. 2019/04/25 14:20:46 ret: 1 error: Access is denied. 2019/04/25 14:20:47 ret: 1 error: Access is denied. 2019/04/25 14:20:48 ret: 1 error: Access is denied. 2019/04/25 14:20:49 ret: 1 error: The operation completed successfully.

The bug ret is always 1 when you are at the secure desktop and SendInput is used in golang, regardless of whether or not an event was dispatched. We know that an event was not dispatched because an error is generated (5 = Access is denied.)

This is worrisome because you are not suppose to check for any errors unless the return value of SendInput is 0.

Remember that this has nothing to do with the syscall calling convention in golang or anything like that. You can put the c code snippet in a dll, call it from a standard C runtime and get a different result than if called from a go runtime.

This bug is quite serious for us because the behavior of user32 changes when interacting with go. It might be the case that other functions also behave erroneously.

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Comments: 44 (23 by maintainers)

Most upvoted comments

produces undocumented changes in the way win32 reacts to syscalls

No, it removes undocumented behavior in exchange for the documented behavior.