- 修正 WTS 会话相关类型、枚举与活动会话选择逻辑 - 对齐 FILE_ID_DESCRIPTOR 布局与 FILE_ID_TYPE 语义,修复 OpenFileById 调用前提 - 修正 user32/shell32/kernel32 部分 API 的返回值、参数个数与错误处理 - 完善剪贴板更新格式读取的缓冲区重试逻辑 - 补充常用进程、线程、调试、桌面与会话 helper - 增加结构体布局、会话查询、剪贴板、CreateProcess 等回归测试 - 将默认 CreateProcess 相关测试切到 helper 进程,并保留显式开启的 cmd.exe 集成覆盖
1479 lines
41 KiB
Go
1479 lines
41 KiB
Go
//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")
|
|
}
|
|
}
|