//go:build windows package win32api import ( "bytes" "os" "path/filepath" "runtime" "strconv" "strings" "syscall" "testing" "time" "unsafe" "golang.org/x/sys/windows" ) const ( processHelperEnv = "WIN32API_TEST_HELPER_PROCESS" processHelperModeEnv = "WIN32API_TEST_HELPER_MODE" processHelperExitEnv = "WIN32API_TEST_HELPER_EXIT_CODE" cmdCoverageEnv = "WIN32API_RUN_CMD_INTEGRATION" processModeExit = "exit" processModeSleep = "sleep" processWaitTime = 30 * time.Second ) func TestProcessHelper(t *testing.T) { if os.Getenv(processHelperEnv) != "1" { return } switch os.Getenv(processHelperModeEnv) { case processModeExit: code, err := strconv.Atoi(os.Getenv(processHelperExitEnv)) if err != nil { os.Exit(2) } os.Exit(code) case processModeSleep: time.Sleep(processWaitTime) os.Exit(0) default: os.Exit(3) } } func helperProcessEnvList(mode string, exitCode int) []string { base := make([]string, 0, len(os.Environ())+3) for _, entry := range os.Environ() { if strings.HasPrefix(entry, processHelperEnv+"=") || strings.HasPrefix(entry, processHelperModeEnv+"=") || strings.HasPrefix(entry, processHelperExitEnv+"=") { continue } base = append(base, entry) } base = append(base, processHelperEnv+"=1", processHelperModeEnv+"="+mode, processHelperExitEnv+"="+strconv.Itoa(exitCode), ) return base } func configureHelperProcess(t *testing.T, mode string, exitCode int) (string, string) { t.Helper() restore := map[string]*string{} for _, key := range []string{processHelperEnv, processHelperModeEnv, processHelperExitEnv} { if value, ok := os.LookupEnv(key); ok { v := value restore[key] = &v } else { restore[key] = nil } } for _, entry := range helperProcessEnvList(mode, exitCode) { parts := strings.SplitN(entry, "=", 2) if len(parts) != 2 { continue } if parts[0] == processHelperEnv || parts[0] == processHelperModeEnv || parts[0] == processHelperExitEnv { if err := os.Setenv(parts[0], parts[1]); err != nil { t.Fatalf("Setenv(%s) failed: %v", parts[0], err) } } } t.Cleanup(func() { for key, value := range restore { if value == nil { _ = os.Unsetenv(key) continue } _ = os.Setenv(key, *value) } }) exe, err := os.Executable() if err != nil { t.Fatalf("Executable failed: %v", err) } return exe, windows.EscapeArg(exe) + " -test.run=^TestProcessHelper$" } func requireCmdCoverage(t *testing.T) { t.Helper() if os.Getenv(cmdCoverageEnv) != "1" { t.Skipf("set %s=1 to run cmd.exe integration coverage", cmdCoverageEnv) } } func TestGetCurrentProcessId(t *testing.T) { pid := GetCurrentProcessId() if pid == 0 { t.Fatal("GetCurrentProcessId returned 0") } if int(pid) != os.Getpid() { t.Fatalf("GetCurrentProcessId mismatch: got=%d want=%d", pid, os.Getpid()) } } func TestGetCurrentThreadId(t *testing.T) { tid := GetCurrentThreadId() if tid == 0 { t.Fatal("GetCurrentThreadId returned 0") } } func TestGetComputerName(t *testing.T) { name, err := GetComputerName() if err != nil { t.Fatalf("GetComputerName failed: %v", err) } name = strings.TrimSpace(name) if name == "" { t.Fatal("GetComputerName returned empty string") } } func TestGetUserName(t *testing.T) { name, err := GetUserName() if err != nil { t.Fatalf("GetUserName failed: %v", err) } name = strings.TrimSpace(name) if name == "" { t.Fatal("GetUserName returned empty string") } } func TestGetTempPath(t *testing.T) { tempPath, err := GetTempPath() if err != nil { t.Fatalf("GetTempPath failed: %v", err) } tempPath = strings.TrimSpace(tempPath) if tempPath == "" { t.Fatal("GetTempPath returned empty string") } cleaned := filepath.Clean(tempPath) if _, err := os.Stat(cleaned); err != nil { t.Fatalf("GetTempPath returned path not found: %q err=%v", cleaned, err) } } func TestGetSystemDirectoryAndGetWindowsDirectory(t *testing.T) { systemDir, err := GetSystemDirectory() if err != nil { t.Fatalf("GetSystemDirectory failed: %v", err) } if strings.TrimSpace(systemDir) == "" { t.Fatal("GetSystemDirectory returned empty path") } if _, err := os.Stat(filepath.Clean(systemDir)); err != nil { t.Fatalf("GetSystemDirectory path not found: %q err=%v", systemDir, err) } windowsDir, err := GetWindowsDirectory() if err != nil { t.Fatalf("GetWindowsDirectory failed: %v", err) } if strings.TrimSpace(windowsDir) == "" { t.Fatal("GetWindowsDirectory returned empty path") } if _, err := os.Stat(filepath.Clean(windowsDir)); err != nil { t.Fatalf("GetWindowsDirectory path not found: %q err=%v", windowsDir, err) } } func TestOpenProcessAndGetExitCodeWait(t *testing.T) { h, err := OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION|SYNCHRONIZE, false, DWORD(os.Getpid())) if err != nil { t.Fatalf("OpenProcess failed: %v", err) } defer func() { _ = CloseHandle(h) }() code, err := GetExitCodeProcess(h) if err != nil { t.Fatalf("GetExitCodeProcess failed: %v", err) } if code != STILL_ACTIVE { t.Fatalf("GetExitCodeProcess should return STILL_ACTIVE(%d), got %d", STILL_ACTIVE, code) } wait, err := WaitForSingleObject(h, 0) if err != nil { t.Fatalf("WaitForSingleObject failed: %v", err) } if wait != WAIT_TIMEOUT { t.Fatalf("WaitForSingleObject should return WAIT_TIMEOUT(%d), got %d", WAIT_TIMEOUT, wait) } } func TestProcess32FirstNextContainsSelf(t *testing.T) { snapshot, err := CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) if err != nil { t.Fatalf("CreateToolhelp32Snapshot failed: %v", err) } defer func() { _ = CloseHandle(snapshot) }() var entry PROCESSENTRY32 entry.DwSize = Ulong(unsafe.Sizeof(entry)) if err := Process32First(snapshot, &entry); err != nil { t.Fatalf("Process32First failed: %v", err) } foundSelf := false count := 0 for { count++ if DWORD(uint32(entry.Th32ProcessID)) == DWORD(os.Getpid()) { foundSelf = true } entry.DwSize = Ulong(unsafe.Sizeof(entry)) err = Process32Next(snapshot, &entry) if err != nil { if errno, ok := err.(syscall.Errno); ok && errno == ERROR_NO_MORE_FILES { break } if err == syscall.EINVAL { break } t.Fatalf("Process32Next failed: %v", err) } } if count == 0 { t.Fatal("process snapshot is empty") } if !foundSelf { t.Fatalf("current process pid=%d not found in process snapshot", os.Getpid()) } } func TestEnumerateProcessesHelper(t *testing.T) { processes, err := EnumerateProcesses() if err != nil { t.Fatalf("EnumerateProcesses failed: %v", err) } if len(processes) == 0 { t.Fatal("EnumerateProcesses returned empty result") } selfExe, _ := os.Executable() selfBase := filepath.Base(selfExe) foundSelf := false for _, entry := range processes { if entry.ExeFile() == "" { t.Fatal("EnumerateProcesses returned process with empty ExeFile") } if selfBase != "" && strings.EqualFold(entry.ExeFile(), selfBase) { foundSelf = true } } if selfBase != "" && !foundSelf { t.Fatalf("current executable %q not found in EnumerateProcesses result", selfBase) } } func TestThread32FirstNextContainsSelf(t *testing.T) { snapshot, err := CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0) if err != nil { t.Fatalf("CreateToolhelp32Snapshot(THREAD) failed: %v", err) } defer func() { _ = CloseHandle(snapshot) }() var entry THREADENTRY32 entry.DwSize = DWORD(unsafe.Sizeof(entry)) if err := Thread32First(snapshot, &entry); err != nil { t.Fatalf("Thread32First failed: %v", err) } currentPID := DWORD(os.Getpid()) currentTID := GetCurrentThreadId() foundProcessThread := false foundCurrentThread := false count := 0 for { count++ if entry.Th32OwnerProcessID == currentPID { foundProcessThread = true if entry.Th32ThreadID == currentTID { foundCurrentThread = true } } entry.DwSize = DWORD(unsafe.Sizeof(entry)) err = Thread32Next(snapshot, &entry) if err != nil { if errno, ok := err.(syscall.Errno); ok && errno == ERROR_NO_MORE_FILES { break } if err == syscall.EINVAL { break } t.Fatalf("Thread32Next failed: %v", err) } } if count == 0 { t.Fatal("thread snapshot is empty") } if !foundProcessThread { t.Fatalf("no thread found for current process pid=%d", currentPID) } if !foundCurrentThread { t.Fatalf("current thread tid=%d not found in thread snapshot", currentTID) } } func TestEnumerateThreadsHelper(t *testing.T) { threads, err := EnumerateThreads(DWORD(os.Getpid())) if err != nil { t.Fatalf("EnumerateThreads failed: %v", err) } if len(threads) == 0 { t.Fatal("EnumerateThreads returned empty result") } currentTID := GetCurrentThreadId() foundCurrentThread := false for _, entry := range threads { if entry.Th32OwnerProcessID != DWORD(os.Getpid()) { t.Fatalf("EnumerateThreads returned thread from another process: got=%d want=%d", entry.Th32OwnerProcessID, os.Getpid()) } if entry.Th32ThreadID == currentTID { foundCurrentThread = true } } if !foundCurrentThread { t.Fatalf("current thread tid=%d not found in EnumerateThreads result", currentTID) } } func TestModule32FirstNextContainsSelf(t *testing.T) { snapshot, err := CreateToolhelp32Snapshot(TH32CS_SNAPMODULE|TH32CS_SNAPMODULE32, DWORD(os.Getpid())) if err != nil { t.Fatalf("CreateToolhelp32Snapshot(MODULE) failed: %v", err) } defer func() { _ = CloseHandle(snapshot) }() var entry MODULEENTRY32W entry.DwSize = DWORD(unsafe.Sizeof(entry)) if err := Module32First(snapshot, &entry); err != nil { t.Fatalf("Module32First failed: %v", err) } selfExe, _ := os.Executable() selfBase := strings.ToLower(filepath.Base(selfExe)) foundSelf := false count := 0 for { count++ exePath := syscall.UTF16ToString(entry.SzExePath[:]) if selfBase != "" && strings.EqualFold(filepath.Base(exePath), selfBase) { foundSelf = true } entry.DwSize = DWORD(unsafe.Sizeof(entry)) err = Module32Next(snapshot, &entry) if err != nil { if errno, ok := err.(syscall.Errno); ok && errno == ERROR_NO_MORE_FILES { break } if err == syscall.EINVAL { break } t.Fatalf("Module32Next failed: %v", err) } } if count == 0 { t.Fatal("module snapshot is empty") } if selfBase != "" && !foundSelf { t.Fatalf("current executable module %q not found in module snapshot", selfBase) } } func TestEnumerateModulesHelper(t *testing.T) { modules, err := EnumerateModules(DWORD(os.Getpid())) if err != nil { t.Fatalf("EnumerateModules failed: %v", err) } if len(modules) == 0 { t.Fatal("EnumerateModules returned empty result") } selfExe, _ := os.Executable() selfBase := filepath.Base(selfExe) foundSelf := false for _, entry := range modules { if entry.ModuleName() == "" { t.Fatal("EnumerateModules returned module with empty ModuleName") } if entry.ExePath() == "" { t.Fatal("EnumerateModules returned module with empty ExePath") } if selfBase != "" && strings.EqualFold(filepath.Base(entry.ExePath()), selfBase) { foundSelf = true } } if selfBase != "" && !foundSelf { t.Fatalf("current executable module %q not found in EnumerateModules result", selfBase) } } func TestOpenThreadCurrentThread(t *testing.T) { tid := GetCurrentThreadId() if tid == 0 { t.Fatal("GetCurrentThreadId returned 0") } threadHandle, err := OpenThread(THREAD_QUERY_INFORMATION, false, tid) if err != nil { t.Fatalf("OpenThread failed: %v", err) } defer func() { _ = CloseHandle(threadHandle) }() } func TestSuspendResumeInvalidThreadHandle(t *testing.T) { if _, err := SuspendThread(0); err == nil { t.Fatal("SuspendThread should fail for invalid handle") } if _, err := ResumeThread(0); err == nil { t.Fatal("ResumeThread should fail for invalid handle") } } func TestQueryFullProcessImageName(t *testing.T) { h, err := OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, DWORD(os.Getpid())) if err != nil { t.Fatalf("OpenProcess failed: %v", err) } defer func() { _ = CloseHandle(h) }() path, err := QueryFullProcessImageName(h, 0) if err != nil { t.Fatalf("QueryFullProcessImageName failed: %v", err) } if strings.TrimSpace(path) == "" { t.Fatal("QueryFullProcessImageName returned empty path") } if _, err := os.Stat(filepath.Clean(path)); err != nil { t.Fatalf("QueryFullProcessImageName path not found: %q err=%v", path, err) } } func TestCreateRemoteThreadSelfExitThread(t *testing.T) { process, err := OpenProcess( PROCESS_CREATE_THREAD|PROCESS_QUERY_INFORMATION|PROCESS_VM_OPERATION|PROCESS_VM_READ|PROCESS_VM_WRITE|SYNCHRONIZE, false, DWORD(os.Getpid()), ) if err != nil { t.Fatalf("OpenProcess failed: %v", err) } defer func() { _ = CloseHandle(process) }() exitThreadProc, err := getProcAddr("kernel32.dll", "ExitThread") if err != nil { t.Fatalf("getProcAddr(ExitThread) failed: %v", err) } var threadID DWORD threadHandle, err := CreateRemoteThread(process, nil, 0, exitThreadProc, 0, 0, &threadID) if err != nil { if errno, ok := err.(syscall.Errno); ok && (errno == syscall.ERROR_ACCESS_DENIED || errno == syscall.Errno(1314)) { t.Skipf("CreateRemoteThread requires higher privilege in current context: %v", err) } t.Fatalf("CreateRemoteThread failed: %v", err) } defer func() { _ = CloseHandle(threadHandle) }() if threadID == 0 { t.Fatal("CreateRemoteThread returned thread id 0") } wait, err := WaitForSingleObject(threadHandle, 5000) if err != nil { t.Fatalf("WaitForSingleObject(thread) failed: %v", err) } if wait != WAIT_OBJECT_0 { t.Fatalf("WaitForSingleObject(thread) mismatch: got=%d want=%d", wait, WAIT_OBJECT_0) } } func TestGetSetThreadContextInvalidArgs(t *testing.T) { if err := GetThreadContext(0, nil); err == nil { t.Fatal("GetThreadContext should fail on nil context pointer") } if err := SetThreadContext(0, nil); err == nil { t.Fatal("SetThreadContext should fail on nil context pointer") } } func TestGetSetThreadContextSuspendedProcess(t *testing.T) { if runtime.GOARCH != "amd64" { t.Skip("GetThreadContext success path test is amd64-only") } app, cmdLine := configureHelperProcess(t, processModeExit, 0) si := StartupInfo{Cb: uint32(unsafe.Sizeof(StartupInfo{}))} pi := ProcessInformation{} if err := CreateProcess( app, cmdLine, nil, nil, false, CREATE_NO_WINDOW|CREATE_SUSPENDED, 0, "", &si, &pi, ); err != nil { t.Fatalf("CreateProcess(CREATE_SUSPENDED) failed: %v", err) } defer func() { if pi.Thread != 0 { _ = CloseHandle(pi.Thread) } if pi.Process != 0 { _ = TerminateProcess(pi.Process, 0) _ = CloseHandle(pi.Process) } }() ctx := AMD64_CONTEXT{ContextFlags: CONTEXT_CONTROL} if err := GetThreadContext(pi.Thread, unsafe.Pointer(&ctx)); err != nil { t.Fatalf("GetThreadContext failed: %v", err) } if ctx.Rip == 0 || ctx.Rsp == 0 { t.Fatalf("GetThreadContext returned empty control registers: RIP=%#x RSP=%#x", ctx.Rip, ctx.Rsp) } if err := SetThreadContext(pi.Thread, unsafe.Pointer(&ctx)); err != nil { t.Fatalf("SetThreadContext failed: %v", err) } if _, err := ResumeThread(pi.Thread); err != nil { t.Fatalf("ResumeThread failed: %v", err) } wait, err := WaitForSingleObject(pi.Process, 5000) if err != nil { t.Fatalf("WaitForSingleObject(process) failed: %v", err) } if wait != WAIT_OBJECT_0 { t.Fatalf("WaitForSingleObject(process) mismatch: got=%d want=%d", wait, WAIT_OBJECT_0) } } func TestDebugActiveProcessAttachWaitContinueStop(t *testing.T) { app, cmdLine := configureHelperProcess(t, processModeSleep, 0) si := StartupInfo{Cb: uint32(unsafe.Sizeof(StartupInfo{}))} pi := ProcessInformation{} if err := CreateProcess( app, cmdLine, nil, nil, false, CREATE_NO_WINDOW, 0, "", &si, &pi, ); err != nil { t.Fatalf("CreateProcess failed: %v", err) } defer func() { if pi.Thread != 0 { _ = CloseHandle(pi.Thread) } if pi.Process != 0 { _ = TerminateProcess(pi.Process, 0) _ = CloseHandle(pi.Process) } }() if err := DebugActiveProcess(DWORD(pi.ProcessId)); err != nil { if errno, ok := err.(syscall.Errno); ok && (errno == syscall.ERROR_ACCESS_DENIED || errno == syscall.Errno(1314)) { t.Skipf("DebugActiveProcess requires higher privilege in current context: %v", err) } t.Fatalf("DebugActiveProcess failed: %v", err) } eventBuffer := make([]byte, 1024) eventInfo, err := WaitForDebugEventInfo(eventBuffer, 5000) if err != nil { if stopErr := DebugActiveProcessStop(DWORD(pi.ProcessId)); stopErr != nil { t.Logf("DebugActiveProcessStop after wait failure: %v", stopErr) } t.Fatalf("WaitForDebugEvent failed: %v", err) } if eventInfo.Header.DwProcessId == 0 || eventInfo.Header.DwThreadId == 0 { t.Fatalf("invalid debug event header: process=%d thread=%d", eventInfo.Header.DwProcessId, eventInfo.Header.DwThreadId) } if eventInfo.CodeName == "" { t.Fatal("WaitForDebugEventInfo returned empty code name") } if err := ContinueDebugEvent(eventInfo.Header.DwProcessId, eventInfo.Header.DwThreadId, DBG_CONTINUE); err != nil { t.Fatalf("ContinueDebugEvent failed: %v", err) } if err := DebugActiveProcessStop(DWORD(pi.ProcessId)); err != nil { if errno, ok := err.(syscall.Errno); ok && errno == syscall.Errno(87) { t.Logf("DebugActiveProcessStop got ERROR_INVALID_PARAMETER (process might already exit): %v", err) } else { t.Fatalf("DebugActiveProcessStop failed: %v", err) } } } func TestWaitForDebugEventAndContinueInvalidArgs(t *testing.T) { if err := WaitForDebugEvent(nil, 0); err == nil { t.Fatal("WaitForDebugEvent should fail on nil event pointer") } if _, err := WaitForDebugEventInfo(nil, 0); err == nil { t.Fatal("WaitForDebugEventInfo should fail on empty buffer") } if err := ContinueDebugEvent(0, 0, DBG_CONTINUE); err == nil { t.Fatal("ContinueDebugEvent should fail on invalid process/thread id") } } func TestDecodeDebugEvent(t *testing.T) { raw := make([]byte, int(unsafe.Sizeof(DEBUG_EVENT_HEADER{}))) header := (*DEBUG_EVENT_HEADER)(unsafe.Pointer(&raw[0])) header.DwDebugEventCode = CREATE_PROCESS_DEBUG_EVENT header.DwProcessId = 123 header.DwThreadId = 456 info, err := DecodeDebugEvent(raw) if err != nil { t.Fatalf("DecodeDebugEvent failed: %v", err) } if info.Header.DwProcessId != 123 || info.Header.DwThreadId != 456 { t.Fatalf("DecodeDebugEvent header mismatch: got process=%d thread=%d", info.Header.DwProcessId, info.Header.DwThreadId) } if info.CodeName != "CREATE_PROCESS_DEBUG_EVENT" { t.Fatalf("DecodeDebugEvent code name mismatch: got=%q", info.CodeName) } if info.String() != "CREATE_PROCESS_DEBUG_EVENT" { t.Fatalf("DebugEventInfo.String mismatch: got=%q", info.String()) } } func TestProcessIdToSessionIdCurrentProcess(t *testing.T) { sessionID, err := ProcessIdToSessionId(DWORD(os.Getpid())) if err != nil { t.Fatalf("ProcessIdToSessionId failed: %v", err) } if sessionID == WTS_CURRENT_SESSION { t.Fatalf("ProcessIdToSessionId returned invalid sentinel value: %d", sessionID) } currentSessionID, err := CurrentProcessSessionID() if err != nil { t.Fatalf("CurrentProcessSessionID failed: %v", err) } if currentSessionID != sessionID { t.Fatalf("session id mismatch: ProcessIdToSessionId=%d CurrentProcessSessionID=%d", sessionID, currentSessionID) } } func TestWTSQuerySessionInformationCurrentProcess(t *testing.T) { sessionID, err := CurrentProcessSessionID() if err != nil { t.Fatalf("CurrentProcessSessionID failed: %v", err) } gotSessionID, err := WTSQuerySessionDWORD(0, sessionID, WTSSessionId) if err != nil { t.Fatalf("WTSQuerySessionDWORD(WTSSessionId) failed: %v", err) } if gotSessionID != sessionID { t.Fatalf("session id mismatch from WTSQuerySessionDWORD: got=%d want=%d", gotSessionID, sessionID) } details, err := GetSessionDetails(0, sessionID) if err != nil { t.Fatalf("GetSessionDetails failed: %v", err) } if details.SessionID != sessionID { t.Fatalf("GetSessionDetails.SessionID mismatch: got=%d want=%d", details.SessionID, sessionID) } if details.ConnectState < WTSActive || details.ConnectState > WTSInit { t.Fatalf("unexpected session connect state: %d", details.ConnectState) } t.Logf( "session details: session=%d user=%q domain=%q winStation=%q connectState=%d remote=%v", details.SessionID, details.UserName, details.DomainName, details.WinStationName, details.ConnectState, details.IsRemote, ) } func TestWin32ScalarTypeSizes(t *testing.T) { if got := unsafe.Sizeof(WTS_CONNECTSTATE_CLASS(0)); got != 4 { t.Fatalf("WTS_CONNECTSTATE_CLASS size mismatch: got=%d want=4", got) } if got := unsafe.Sizeof(WTS_INFO_CLASS(0)); got != 4 { t.Fatalf("WTS_INFO_CLASS size mismatch: got=%d want=4", got) } if got := unsafe.Sizeof(TOKEN_TYPE(0)); got != 4 { t.Fatalf("TOKEN_TYPE size mismatch: got=%d want=4", got) } if got := unsafe.Sizeof(SECURITY_IMPERSONATION_LEVEL(0)); got != 4 { t.Fatalf("SECURITY_IMPERSONATION_LEVEL size mismatch: got=%d want=4", got) } if got := unsafe.Sizeof(SW(0)); got != 4 { t.Fatalf("SW size mismatch: got=%d want=4", got) } } func TestWTSSessionInfoLayout(t *testing.T) { wantNameOffset := uintptr(4) wantStateOffset := uintptr(8) wantSize := uintptr(12) if unsafe.Sizeof(uintptr(0)) == 8 { wantNameOffset = 8 wantStateOffset = 16 wantSize = 24 } if got := unsafe.Sizeof(WTS_SESSION_INFO{}); got != wantSize { t.Fatalf("WTS_SESSION_INFO size mismatch: got=%d want=%d", got, wantSize) } if got := unsafe.Offsetof(WTS_SESSION_INFO{}.WinStationName); got != wantNameOffset { t.Fatalf("WTS_SESSION_INFO.WinStationName offset mismatch: got=%d want=%d", got, wantNameOffset) } if got := unsafe.Offsetof(WTS_SESSION_INFO{}.State); got != wantStateOffset { t.Fatalf("WTS_SESSION_INFO.State offset mismatch: got=%d want=%d", got, wantStateOffset) } } func TestShellExecuteInfoLayout(t *testing.T) { wantNShowOffset := uintptr(28) wantHProcessOffset := uintptr(52) wantSize := uintptr(56) if unsafe.Sizeof(uintptr(0)) == 8 { wantNShowOffset = 48 wantHProcessOffset = 104 wantSize = 112 } if got := unsafe.Sizeof(SHELLEXECUTEINFOW{}.NShow); got != 4 { t.Fatalf("SHELLEXECUTEINFOW.NShow size mismatch: got=%d want=4", got) } if got := unsafe.Offsetof(SHELLEXECUTEINFOW{}.NShow); got != wantNShowOffset { t.Fatalf("SHELLEXECUTEINFOW.NShow offset mismatch: got=%d want=%d", got, wantNShowOffset) } if got := unsafe.Offsetof(SHELLEXECUTEINFOW{}.HProcess); got != wantHProcessOffset { t.Fatalf("SHELLEXECUTEINFOW.HProcess offset mismatch: got=%d want=%d", got, wantHProcessOffset) } if got := unsafe.Sizeof(SHELLEXECUTEINFOW{}); got != wantSize { t.Fatalf("SHELLEXECUTEINFOW size mismatch: got=%d want=%d", got, wantSize) } } func TestFileIDDescriptorLayout(t *testing.T) { if got := unsafe.Sizeof(FILE_ID_TYPE(0)); got != 4 { t.Fatalf("FILE_ID_TYPE size mismatch: got=%d want=4", got) } if got := unsafe.Offsetof(FILE_ID_DESCRIPTOR{}.FileId); got != 8 { t.Fatalf("FILE_ID_DESCRIPTOR.FileId offset mismatch: got=%d want=8", got) } if got := unsafe.Sizeof(FILE_ID_DESCRIPTOR{}); got != 24 { t.Fatalf("FILE_ID_DESCRIPTOR size mismatch: got=%d want=24", got) } } func TestEnumerateSessionsAndActiveSessionID(t *testing.T) { currentSessionID, err := CurrentProcessSessionID() if err != nil { t.Fatalf("CurrentProcessSessionID failed: %v", err) } sessions, err := EnumerateSessions() if err != nil { t.Fatalf("EnumerateSessions failed: %v", err) } if len(sessions) == 0 { t.Fatal("EnumerateSessions returned empty result") } foundCurrent := false activeIDs := make(map[DWORD]bool) for _, session := range sessions { if session.SessionID == currentSessionID { foundCurrent = true } if session.State < WTSActive || session.State > WTSInit { t.Fatalf("unexpected session state %d for session %d", session.State, session.SessionID) } if session.State == WTSActive { activeIDs[session.SessionID] = true } } if !foundCurrent { t.Fatalf("current session id %d not found in EnumerateSessions result", currentSessionID) } activeSessionID, err := ActiveSessionID() if err != nil { t.Fatalf("ActiveSessionID failed: %v", err) } if activeSessionID == WTS_CURRENT_SESSION { t.Fatalf("ActiveSessionID returned invalid sentinel value: %d", activeSessionID) } if len(activeIDs) > 0 && !activeIDs[activeSessionID] { t.Fatalf("ActiveSessionID=%d not present in active session set %v", activeSessionID, activeIDs) } } func TestVirtualAllocReadWriteProtectFreeSelf(t *testing.T) { process, err := OpenProcess(PROCESS_VM_OPERATION|PROCESS_VM_READ|PROCESS_VM_WRITE|PROCESS_QUERY_INFORMATION, false, DWORD(os.Getpid())) if err != nil { t.Fatalf("OpenProcess failed: %v", err) } defer func() { _ = CloseHandle(process) }() const allocSize = uintptr(4096) addr, err := VirtualAllocEx(process, 0, allocSize, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE) if err != nil { t.Fatalf("VirtualAllocEx failed: %v", err) } defer func() { _ = VirtualFreeEx(process, addr, 0, MEM_RELEASE) }() payload := []byte("win32api-memory-rw") var written uintptr if err := WriteProcessMemory(process, addr, payload, &written); err != nil { t.Fatalf("WriteProcessMemory failed: %v", err) } if written != uintptr(len(payload)) { t.Fatalf("WriteProcessMemory written mismatch: got=%d want=%d", written, len(payload)) } readBuf := make([]byte, len(payload)) var read uintptr if err := ReadProcessMemory(process, addr, readBuf, &read); err != nil { t.Fatalf("ReadProcessMemory failed: %v", err) } if read != uintptr(len(payload)) { t.Fatalf("ReadProcessMemory read mismatch: got=%d want=%d", read, len(payload)) } if string(readBuf) != string(payload) { t.Fatalf("ReadProcessMemory content mismatch: got=%q want=%q", string(readBuf), string(payload)) } var oldProtect DWORD if err := VirtualProtectEx(process, addr, uintptr(len(payload)), PAGE_READONLY, &oldProtect); err != nil { t.Fatalf("VirtualProtectEx(PAGE_READONLY) failed: %v", err) } var restoreOld DWORD if err := VirtualProtectEx(process, addr, uintptr(len(payload)), oldProtect, &restoreOld); err != nil { t.Fatalf("VirtualProtectEx(restore) failed: %v", err) } } func TestVirtualQueryExSelfAllocatedRegion(t *testing.T) { process, err := OpenProcess(PROCESS_VM_OPERATION|PROCESS_VM_READ|PROCESS_QUERY_INFORMATION, false, DWORD(os.Getpid())) if err != nil { t.Fatalf("OpenProcess failed: %v", err) } defer func() { _ = CloseHandle(process) }() const allocSize = uintptr(4096) addr, err := VirtualAllocEx(process, 0, allocSize, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE) if err != nil { t.Fatalf("VirtualAllocEx failed: %v", err) } defer func() { _ = VirtualFreeEx(process, addr, 0, MEM_RELEASE) }() var mbi MEMORY_BASIC_INFORMATION readLen, err := VirtualQueryEx(process, addr, &mbi, uintptr(unsafe.Sizeof(mbi))) if err != nil { t.Fatalf("VirtualQueryEx failed: %v", err) } if readLen == 0 { t.Fatal("VirtualQueryEx returned 0 bytes") } if mbi.RegionSize == 0 { t.Fatal("VirtualQueryEx returned RegionSize=0") } if mbi.State != MEM_COMMIT { t.Fatalf("VirtualQueryEx state mismatch: got=%#x want=%#x", mbi.State, MEM_COMMIT) } if addr < mbi.BaseAddress || addr-mbi.BaseAddress >= mbi.RegionSize { t.Fatalf("allocated address %#x is outside queried region [%#x, %#x)", addr, mbi.BaseAddress, mbi.BaseAddress+mbi.RegionSize) } } func TestGetFileAttributesAndGetFullPathName(t *testing.T) { tmpDir := t.TempDir() relative := filepath.Join(tmpDir, ".", "attrs.txt") if err := os.WriteFile(relative, []byte("ok"), 0644); err != nil { t.Fatalf("WriteFile failed: %v", err) } attrs, err := GetFileAttributes(relative) if err != nil { t.Fatalf("GetFileAttributes failed: %v", err) } if attrs == INVALID_FILE_ATTRIBUTES { t.Fatalf("GetFileAttributes returned INVALID_FILE_ATTRIBUTES(%d)", INVALID_FILE_ATTRIBUTES) } full, err := GetFullPathName(relative) if err != nil { t.Fatalf("GetFullPathName failed: %v", err) } if full == "" { t.Fatal("GetFullPathName returned empty string") } if !strings.EqualFold(filepath.Clean(full), filepath.Clean(relative)) { t.Fatalf("GetFullPathName mismatch: got=%q want=%q", filepath.Clean(full), filepath.Clean(relative)) } } func TestMoveFileEx(t *testing.T) { tmpDir := t.TempDir() src := filepath.Join(tmpDir, "src.txt") dst := filepath.Join(tmpDir, "dst.txt") if err := os.WriteFile(src, []byte("move-me"), 0644); err != nil { t.Fatalf("WriteFile src failed: %v", err) } if err := os.WriteFile(dst, []byte("existing"), 0644); err != nil { t.Fatalf("WriteFile dst failed: %v", err) } if err := MoveFileEx(src, dst, MOVEFILE_REPLACE_EXISTING); err != nil { t.Fatalf("MoveFileEx failed: %v", err) } if _, err := os.Stat(src); !os.IsNotExist(err) { t.Fatalf("source should be gone after MoveFileEx, stat err=%v", err) } data, err := os.ReadFile(dst) if err != nil { t.Fatalf("ReadFile dst failed: %v", err) } if string(data) != "move-me" { t.Fatalf("unexpected dst content after MoveFileEx: %q", string(data)) } } func TestGetLastErrorAndFormatMessage(t *testing.T) { runtime.LockOSThread() defer runtime.UnlockOSThread() missing := filepath.Join(t.TempDir(), "missing.txt") delErr := DeleteFile(missing) if delErr == nil { t.Fatal("DeleteFile should fail for missing file") } code := GetLastError() if code == 0 { // In Go, runtime/syscall interaction may clear the thread-local last-error // before we query it again; fall back to the direct syscall error. if errno, ok := delErr.(syscall.Errno); ok { code = DWORD(errno) } } if code == 0 { code = DWORD(syscall.ERROR_FILE_NOT_FOUND) } if errno, ok := delErr.(syscall.Errno); ok && code != DWORD(errno) { t.Logf("GetLastError differs from direct syscall errno: last=%d errno=%d", code, errno) if code == 0 { code = DWORD(errno) } } msg, err := FormatMessage(code) if err != nil { t.Fatalf("FormatMessage failed: %v", err) } if strings.TrimSpace(msg) == "" { t.Fatal("FormatMessage returned empty message") } } func TestRtlMoveMemoryIgnoresStaleLastError(t *testing.T) { runtime.LockOSThread() defer runtime.UnlockOSThread() if err := SetLastError(DWORD(syscall.ERROR_ACCESS_DENIED)); err != nil { t.Fatalf("SetLastError failed: %v", err) } src := []byte("copy-ok") dst := make([]byte, len(src)) if err := RtlMoveMemory(unsafe.Pointer(&dst[0]), unsafe.Pointer(&src[0]), uintptr(len(src))); err != nil { t.Fatalf("RtlMoveMemory failed with stale last error: %v", err) } if !bytes.Equal(dst, src) { t.Fatalf("RtlMoveMemory copy mismatch: got=%q want=%q", string(dst), string(src)) } } func TestCountClipboardFormatsEmptyClipboard(t *testing.T) { if err := OpenClipboard(0); err != nil { if errno, ok := err.(syscall.Errno); ok && errno == syscall.ERROR_ACCESS_DENIED { t.Skipf("OpenClipboard access denied in current context: %v", err) } t.Fatalf("OpenClipboard failed: %v", err) } defer func() { if err := CloseClipboard(); err != nil { t.Fatalf("CloseClipboard failed: %v", err) } }() if err := EmptyClipboard(); err != nil { t.Fatalf("EmptyClipboard failed: %v", err) } count, err := CountClipboardFormats() if err != nil { t.Fatalf("CountClipboardFormats failed: %v", err) } if count != 0 { t.Fatalf("CountClipboardFormats = %d, want 0", count) } } func TestGetClipboardOwnerEmptyClipboard(t *testing.T) { if err := OpenClipboard(0); err != nil { if errno, ok := err.(syscall.Errno); ok && errno == syscall.ERROR_ACCESS_DENIED { t.Skipf("OpenClipboard access denied in current context: %v", err) } t.Fatalf("OpenClipboard failed: %v", err) } defer func() { if err := CloseClipboard(); err != nil { t.Fatalf("CloseClipboard failed: %v", err) } }() if err := EmptyClipboard(); err != nil { t.Fatalf("EmptyClipboard failed: %v", err) } owner, err := GetClipboardOwner() if err != nil { t.Fatalf("GetClipboardOwner failed: %v", err) } if owner != 0 { t.Fatalf("GetClipboardOwner = %#x, want 0", owner) } } func TestCreateFileReadWriteCopyDelete(t *testing.T) { tmpDir := t.TempDir() src := filepath.Join(tmpDir, "io_src.txt") dst := filepath.Join(tmpDir, "io_dst.txt") h, err := CreateFile( src, GENERIC_WRITE, FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE, nil, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0, ) if err != nil { t.Fatalf("CreateFile write failed: %v", err) } defer func() { _ = CloseHandle(h) }() payload := []byte("hello-win32api") var written DWORD if err := WriteFile(h, payload, &written, nil); err != nil { t.Fatalf("WriteFile failed: %v", err) } if int(written) != len(payload) { t.Fatalf("WriteFile bytes mismatch: got=%d want=%d", written, len(payload)) } if err := CloseHandle(h); err != nil { t.Fatalf("CloseHandle write file failed: %v", err) } h = 0 rh, err := CreateFile( src, GENERIC_READ, FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE, nil, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0, ) if err != nil { t.Fatalf("CreateFile read failed: %v", err) } defer func() { _ = CloseHandle(rh) }() buf := make([]byte, 64) var read DWORD if err := ReadFile(rh, buf, &read, nil); err != nil { t.Fatalf("ReadFile failed: %v", err) } if string(buf[:read]) != string(payload) { t.Fatalf("ReadFile content mismatch: got=%q want=%q", string(buf[:read]), string(payload)) } if err := CopyFile(src, dst, false); err != nil { t.Fatalf("CopyFile failed: %v", err) } copied, err := os.ReadFile(dst) if err != nil { t.Fatalf("Read copied file failed: %v", err) } if string(copied) != string(payload) { t.Fatalf("CopyFile content mismatch: got=%q want=%q", string(copied), string(payload)) } if err := DeleteFile(dst); err != nil { t.Fatalf("DeleteFile dst failed: %v", err) } if err := DeleteFile(src); err != nil { t.Fatalf("DeleteFile src failed: %v", err) } if _, err := os.Stat(dst); !os.IsNotExist(err) { t.Fatalf("dst should not exist, stat err=%v", err) } if _, err := os.Stat(src); !os.IsNotExist(err) { t.Fatalf("src should not exist, stat err=%v", err) } } func TestWaitForMultipleObjects(t *testing.T) { ev1, err := CreateEventW(nil, true, true, nil) if err != nil { t.Fatalf("CreateEventW ev1 failed: %v", err) } defer func() { _ = CloseHandle(ev1) }() ev2, err := CreateEventW(nil, true, false, nil) if err != nil { t.Fatalf("CreateEventW ev2 failed: %v", err) } defer func() { _ = CloseHandle(ev2) }() waitAny, err := WaitForMultipleObjects([]HANDLE{ev1, ev2}, false, 0) if err != nil { t.Fatalf("WaitForMultipleObjects(wait any) failed: %v", err) } if waitAny != WAIT_OBJECT_0 { t.Fatalf("WaitForMultipleObjects(wait any) mismatch: got=%d want=%d", waitAny, WAIT_OBJECT_0) } waitAll, err := WaitForMultipleObjects([]HANDLE{ev1, ev2}, true, 0) if err != nil { t.Fatalf("WaitForMultipleObjects(wait all) failed: %v", err) } if waitAll != WAIT_TIMEOUT { t.Fatalf("WaitForMultipleObjects(wait all) mismatch: got=%d want=%d", waitAll, WAIT_TIMEOUT) } } func TestOpenFileByIdInvalidHandleFails(t *testing.T) { desc := FILE_ID_DESCRIPTOR{ DwSize: DWORD(unsafe.Sizeof(FILE_ID_DESCRIPTOR{})), Type: FileIdType, FileId: 1, } h, err := OpenFileById(0, &desc, GENERIC_READ, FILE_SHARE_READ|FILE_SHARE_WRITE, nil, FILE_ATTRIBUTE_NORMAL) if err == nil { if h != 0 && h != HANDLE(syscall.InvalidHandle) { _ = CloseHandle(h) } t.Fatalf("OpenFileById with invalid volume handle returned nil error and handle=%#x", h) } if h != HANDLE(syscall.InvalidHandle) { t.Fatalf("OpenFileById invalid handle result=%#x, want INVALID_HANDLE_VALUE", h) } } func TestGlobalUnlockZeroReturnCanMeanSuccess(t *testing.T) { mem, err := GlobalAlloc(GMEM_MOVEABLE|GMEM_ZEROINIT, 16) if err != nil { t.Fatalf("GlobalAlloc failed: %v", err) } defer func() { _ = GlobalFree(mem) }() ptr, err := GlobalLock(mem) if err != nil { t.Fatalf("GlobalLock failed: %v", err) } if ptr == nil { t.Fatal("GlobalLock returned nil pointer") } if err := GlobalUnlock(mem); err != nil { t.Fatalf("GlobalUnlock should succeed when lock count reaches zero: %v", err) } } func TestOpenProcessTokenLookupPrivilegeAdjustRevert(t *testing.T) { processHandle, err := syscall.GetCurrentProcess() if err != nil { t.Fatalf("GetCurrentProcess failed: %v", err) } var token TOKEN if err := OpenProcessToken(HANDLE(processHandle), TOKEN_QUERY|TOKEN_ADJUST_PRIVILEGES, &token); err != nil { t.Fatalf("OpenProcessToken failed: %v", err) } defer func() { _ = CloseHandle(HANDLE(token)) }() var luid LUID if err := LookupPrivilegeValue("", SE_CHANGE_NOTIFY_NAME, &luid); err != nil { t.Fatalf("LookupPrivilegeValue failed: %v", err) } tp := TOKEN_PRIVILEGES{ PrivilegeCount: 1, Privileges: [1]LUID_AND_ATTRIBUTES{ { Luid: luid, Attributes: SE_PRIVILEGE_ENABLED, }, }, } if err := AdjustTokenPrivileges(token, false, &tp, 0, nil, nil); err != nil { t.Fatalf("AdjustTokenPrivileges failed: %v", err) } if err := RevertToSelf(); err != nil { t.Fatalf("RevertToSelf failed: %v", err) } } func TestIsCurrentProcessElevated(t *testing.T) { elevated, err := IsCurrentProcessElevated() if err != nil { t.Fatalf("IsCurrentProcessElevated failed: %v", err) } t.Logf("current process elevated: %v", elevated) } func TestIsCurrentUserInAdminGroup(t *testing.T) { isAdmin, err := IsCurrentUserInAdminGroup() if err != nil { t.Fatalf("IsCurrentUserInAdminGroup failed: %v", err) } t.Logf("current user in administrators group: %v", isAdmin) } func TestCreateProcessAsUserCreationFlagsTypeCoversDWORD(t *testing.T) { var flags DWORD = CREATE_NO_WINDOW | CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_CONSOLE if flags&CREATE_NO_WINDOW == 0 { t.Fatal("CREATE_NO_WINDOW should be preserved in DWORD creation flags") } } func TestCreateProcess(t *testing.T) { app, cmdLine := configureHelperProcess(t, processModeExit, 0) si := StartupInfo{ Cb: uint32(unsafe.Sizeof(StartupInfo{})), } pi := ProcessInformation{} if err := CreateProcess( app, cmdLine, nil, nil, false, CREATE_NO_WINDOW, 0, "", &si, &pi, ); err != nil { t.Fatalf("CreateProcess failed: %v", err) } defer func() { if pi.Thread != 0 { _ = CloseHandle(pi.Thread) } if pi.Process != 0 { _ = CloseHandle(pi.Process) } }() wait, err := WaitForSingleObject(pi.Process, 5000) if err != nil { t.Fatalf("WaitForSingleObject failed: %v", err) } if wait != WAIT_OBJECT_0 { t.Fatalf("WaitForSingleObject mismatch: got=%d want=%d", wait, WAIT_OBJECT_0) } code, err := GetExitCodeProcess(pi.Process) if err != nil { t.Fatalf("GetExitCodeProcess failed: %v", err) } if code != 0 { t.Fatalf("CreateProcess exit code mismatch: got=%d want=0", code) } } func TestCreateProcessCmdIntegration(t *testing.T) { requireCmdCoverage(t) si := StartupInfo{ Cb: uint32(unsafe.Sizeof(StartupInfo{})), } pi := ProcessInformation{} if err := CreateProcess( "", "cmd.exe /C exit 0", nil, nil, false, CREATE_NO_WINDOW, 0, "", &si, &pi, ); err != nil { t.Fatalf("CreateProcess(cmd.exe) failed: %v", err) } defer func() { if pi.Thread != 0 { _ = CloseHandle(pi.Thread) } if pi.Process != 0 { _ = CloseHandle(pi.Process) } }() wait, err := WaitForSingleObject(pi.Process, 5000) if err != nil { t.Fatalf("WaitForSingleObject(cmd.exe) failed: %v", err) } if wait != WAIT_OBJECT_0 { t.Fatalf("WaitForSingleObject mismatch: got=%d want=%d", wait, WAIT_OBJECT_0) } } func TestCreateProcessWithToken(t *testing.T) { app, cmdLine := configureHelperProcess(t, processModeExit, 0) processHandle, err := syscall.GetCurrentProcess() if err != nil { t.Fatalf("GetCurrentProcess failed: %v", err) } var token TOKEN desired := TOKEN_QUERY | TOKEN_DUPLICATE | TOKEN_ASSIGN_PRIMARY if err := OpenProcessToken(HANDLE(processHandle), desired, &token); err != nil { t.Fatalf("OpenProcessToken for CreateProcessWithToken failed: %v", err) } defer func() { _ = CloseHandle(HANDLE(token)) }() si := StartupInfo{ Cb: uint32(unsafe.Sizeof(StartupInfo{})), } pi := ProcessInformation{} err = CreateProcessWithToken( token, 0, app, cmdLine, CREATE_NO_WINDOW, 0, "", &si, &pi, ) if err != nil { if errno, ok := err.(syscall.Errno); ok { if errno == 1314 || errno == syscall.ERROR_ACCESS_DENIED { t.Skipf("CreateProcessWithToken requires extra privilege in current context: %v", err) } } t.Fatalf("CreateProcessWithToken failed: %v", err) } defer func() { if pi.Thread != 0 { _ = CloseHandle(pi.Thread) } if pi.Process != 0 { _ = CloseHandle(pi.Process) } }() wait, err := WaitForSingleObject(pi.Process, 5000) if err != nil { t.Fatalf("WaitForSingleObject(CreateProcessWithToken) failed: %v", err) } if wait != WAIT_OBJECT_0 { t.Fatalf("CreateProcessWithToken wait mismatch: got=%d want=%d", wait, WAIT_OBJECT_0) } code, err := GetExitCodeProcess(pi.Process) if err != nil { t.Fatalf("GetExitCodeProcess(CreateProcessWithToken) failed: %v", err) } if code != 0 { t.Fatalf("CreateProcessWithToken exit code mismatch: got=%d want=0", code) } } func TestShellExecuteExRejectsInvalidStruct(t *testing.T) { var info SHELLEXECUTEINFOW if err := ShellExecuteEx(&info); err == nil { t.Fatal("expected ShellExecuteEx to reject zero-value struct") } } func TestCreateEnvironmentBlockRejectsNilOutput(t *testing.T) { err := CreateEnvironmentBlock(nil, 0, 0) if err == nil { t.Fatal("expected CreateEnvironmentBlock to reject nil output pointer") } }