项目文件夹

文件
wehub-resource-sync 36b3af2e3d
PR Check / Code Quality: Format (push) Failing after 1s
PR Check / Code Quality: Lint (darwin) (push) Failing after 0s
PR Check / Code Quality: Lint (freebsd) (push) Failing after 1s
PR Check / Code Quality: Lint (windows) (push) Failing after 1s
PR Check / Code Quality: Lint (linux) (push) Failing after 1s
PR Check / Security: Vulnerability Scan (push) Failing after 0s
Update Documentation / update-docs (push) Failing after 2s
PR Check / Code Quality: Vendor (push) Failing after 1s
PR Check / Code Quality: Coverage (push) Failing after 0s
PR Check / Tests: Unit (macos-latest) (push) Has been cancelled
PR Check / Tests: Unit (ubuntu-24.04) (push) Has been cancelled
PR Check / Tests: Unit (ubuntu-24.04-arm) (push) Has been cancelled
PR Check / Tests: Unit (windows-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:22:06 +08:00

369 行
11 KiB
Go

//go:build windows
package proc
import (
"fmt"
"path/filepath"
"syscall"
"time"
"unsafe"
)
// Win32 API constants and structures
const (
PROCESS_QUERY_INFORMATION = 0x0400
PROCESS_VM_READ = 0x0010
PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
TH32CS_SNAPPROCESS = 0x00000002
)
var (
modntdll = syscall.NewLazyDLL("ntdll.dll")
procNtQueryInfo = modntdll.NewProc("NtQueryInformationProcess")
modkernel32 = syscall.NewLazyDLL("kernel32.dll")
procReadProcessMem = modkernel32.NewProc("ReadProcessMemory")
procGetProcessTimes = modkernel32.NewProc("GetProcessTimes")
procQueryFullProcessImageName = modkernel32.NewProc("QueryFullProcessImageNameW")
procCreateToolhelp32Snapshot = modkernel32.NewProc("CreateToolhelp32Snapshot")
procProcess32First = modkernel32.NewProc("Process32FirstW")
procProcess32Next = modkernel32.NewProc("Process32NextW")
)
type processBasicInformation struct {
ExitStatus uintptr
PebBaseAddress uintptr
AffinityMask uintptr
BasePriority uintptr
UniqueProcessId uintptr
InheritedFromUniqueProcessId uintptr
}
type unicodeString struct {
Length uint16
MaximumLength uint16
Buffer uintptr
}
// Partial RTL_USER_PROCESS_PARAMETERS
type rtlUserProcessParameters struct {
Reserved1 [16]byte
Reserved2 [5]uintptr
CurrentDirectoryPath unicodeString
CurrentDirectoryHandle uintptr
DllPath unicodeString
ImagePathName unicodeString
CommandLine unicodeString
Environment uintptr
}
type PROCESSENTRY32 struct {
Size uint32
CntUsage uint32
ProcessID uint32
DefaultHeapID uintptr
ModuleID uint32
CntThreads uint32
ParentProcessID uint32
PriClassBase int32
Flags uint32
ExeFile [260]uint16
}
type Win32ProcessInfo struct {
PPID int
CommandLine string
Exe string
Cwd string
Env []string
StartedAt time.Time
}
func GetProcessDetailedInfo(pid int) (Win32ProcessInfo, error) {
var info Win32ProcessInfo
// 1. Try Full Access (Query Info + VM Read)
handle, err := syscall.OpenProcess(PROCESS_QUERY_INFORMATION|PROCESS_VM_READ, false, uint32(pid))
if err == nil {
defer syscall.CloseHandle(handle)
err := getFullProcessInfo(handle, pid, &info)
if err == nil {
return info, nil
}
// If getFullProcessInfo fails (e.g. PEB read error), fall through to limited
}
// 2. Fallback: Try Limited Access (Query Limited Info)
// This allows getting Exe Path and Start Time for elevated processes from standard user.
handleLimited, err := syscall.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, uint32(pid))
if err != nil {
// Fallback: If we can't open the process (Access Denied), try getting basic info from the snapshot.
ppid, exe, snapErr := getInfoFromSnapshot(pid)
if snapErr == nil {
info.PPID = ppid
info.Exe = exe
info.CommandLine = exe
return info, nil
}
return info, err
}
defer syscall.CloseHandle(handleLimited)
// Get Start Time
info.StartedAt = getProcessStartTime(handleLimited)
// Get PPID and a fallback image name from the snapshot.
ppid, snapExe, _ := getInfoFromSnapshot(pid)
info.PPID = ppid
// Prefer the full image path. Fall back to the snapshot's bare image name
// for minimal/system processes (System, Memory Compression, vmmemWSL) whose
// path can't be queried, so the name still resolves instead of being blank.
exePath := getProcessImageName(handleLimited)
if exePath == "" {
exePath = snapExe
}
info.Exe = exePath
if exePath != "" {
info.CommandLine = filepath.Base(exePath)
}
// Cwd and Env are unavailable without VM_READ
info.Cwd = ""
info.Env = []string{}
return info, nil
}
func getFullProcessInfo(handle syscall.Handle, pid int, info *Win32ProcessInfo) error {
info.StartedAt = getProcessStartTime(handle)
var pbi processBasicInformation
var returnLength uint32
status, _, _ := procNtQueryInfo.Call(
uintptr(handle),
0, // ProcessBasicInformation
uintptr(unsafe.Pointer(&pbi)),
uintptr(unsafe.Sizeof(pbi)),
uintptr(unsafe.Pointer(&returnLength)),
)
if status != 0 {
return fmt.Errorf("NtQueryInformationProcess failed with status %x", status)
}
info.PPID = int(pbi.InheritedFromUniqueProcessId)
if pbi.PebBaseAddress == 0 {
return fmt.Errorf("PEB Base Address is 0")
}
// Read PEB
var pebPtr uintptr
paramsOffset := uintptr(0x20)
if unsafe.Sizeof(uintptr(0)) == 4 {
paramsOffset = 0x10
}
if !readProcessMemory(handle, pbi.PebBaseAddress+paramsOffset, unsafe.Pointer(&pebPtr), unsafe.Sizeof(pebPtr)) {
return fmt.Errorf("failed to read PEB ProcessParameters address")
}
var params rtlUserProcessParameters
if !readProcessMemory(handle, pebPtr, unsafe.Pointer(&params), unsafe.Sizeof(params)) {
return fmt.Errorf("failed to read ProcessParameters struct")
}
info.Cwd = readUnicodeString(handle, params.CurrentDirectoryPath)
info.CommandLine = readUnicodeString(handle, params.CommandLine)
info.Exe = readUnicodeString(handle, params.ImagePathName)
info.Env = readEnvironmentBlock(handle, params.Environment)
return nil
}
func readProcessMemory(handle syscall.Handle, addr uintptr, dest unsafe.Pointer, size uintptr) bool {
// lpNumberOfBytesRead is a SIZE_T* (pointer-sized: 8 bytes on x64). It MUST
// be uintptr, not uint32 — a uint32 here lets the kernel write 8 bytes into
// a 4-byte slot, corrupting adjacent memory and causing nondeterministic
// crashes far from this call site.
var read uintptr
ret, _, _ := procReadProcessMem.Call(
uintptr(handle),
addr,
uintptr(dest),
size,
uintptr(unsafe.Pointer(&read)),
)
return ret != 0
}
// readEnvironmentBlock reads a process's environment from its PEB. The block is
// a run of "KEY=VALUE\0" entries terminated by an empty entry (a \0\0). It is
// read in chunks until that terminator appears or a read fails, and bounded so a
// corrupt pointer can't drive an unbounded read of remote memory.
func readEnvironmentBlock(handle syscall.Handle, addr uintptr) []string {
if addr == 0 {
return nil
}
const chunkWords = 2048 // 4 KiB per read
const maxWords = 64 * 1024 // cap at 128 KiB
var block []uint16
for len(block) < maxWords {
buf := make([]uint16, chunkWords)
if !readProcessMemory(handle, addr+uintptr(len(block)*2), unsafe.Pointer(&buf[0]), uintptr(len(buf)*2)) {
break
}
block = append(block, buf...)
if envBlockEnd(block) >= 0 {
break
}
}
return parseEnvBlock(block)
}
// envBlockEnd returns the index of the \0\0 terminator, or -1 if not yet read.
func envBlockEnd(block []uint16) int {
for i := 0; i+1 < len(block); i++ {
if block[i] == 0 && block[i+1] == 0 {
return i
}
}
return -1
}
// parseEnvBlock splits the environment block into "KEY=VALUE" entries, stopping
// at the empty entry that terminates it.
func parseEnvBlock(block []uint16) []string {
var env []string
start := 0
for i := 0; i < len(block); i++ {
if block[i] != 0 {
continue
}
if i == start { // empty entry: end of block
break
}
env = append(env, syscall.UTF16ToString(block[start:i]))
start = i + 1
}
return env
}
func readUnicodeString(handle syscall.Handle, us unicodeString) string {
// us.Length is a byte count. Read only whole uint16 code units, and never
// more bytes than the destination buffer holds: a malformed (odd or partial)
// Length from an incomplete PEB read must not overrun buf.
n := int(us.Length) / 2
if n == 0 || us.Buffer == 0 {
return ""
}
buf := make([]uint16, n)
if !readProcessMemory(handle, us.Buffer, unsafe.Pointer(&buf[0]), uintptr(n*2)) {
return ""
}
return syscall.UTF16ToString(buf)
}
func getProcessStartTime(handle syscall.Handle) time.Time {
var creation, exit, kernel, user syscall.Filetime
ret, _, _ := procGetProcessTimes.Call(
uintptr(handle),
uintptr(unsafe.Pointer(&creation)),
uintptr(unsafe.Pointer(&exit)),
uintptr(unsafe.Pointer(&kernel)),
uintptr(unsafe.Pointer(&user)),
)
if ret == 0 {
return time.Time{}
}
return time.Unix(0, creation.Nanoseconds())
}
func getProcessImageName(handle syscall.Handle) string {
buf := make([]uint16, 1024)
size := uint32(len(buf))
// QueryFullProcessImageNameW(hProcess, 0, lpExeName, lpdwSize)
ret, _, _ := procQueryFullProcessImageName.Call(
uintptr(handle),
0,
uintptr(unsafe.Pointer(&buf[0])),
uintptr(unsafe.Pointer(&size)),
)
if ret == 0 {
return ""
}
return syscall.UTF16ToString(buf[:size])
}
func getInfoFromSnapshot(pid int) (int, string, error) {
procs, err := enumerateProcesses()
if err != nil {
return 0, "", err
}
for _, p := range procs {
if p.PID == pid {
return p.PPID, p.Exe, nil
}
}
return 0, "", fmt.Errorf("process %d not found in snapshot", pid)
}
// processCommandLineInformation is the NtQueryInformationProcess class (60,
// Windows 8.1+) that returns a process's command line.
const processCommandLineInformation = 60
// windowsProcessCmdline returns a process's full command line via
// NtQueryInformationProcess(ProcessCommandLineInformation). The kernel copies
// the command line into our own buffer, so — unlike walking the PEB with
// ReadProcessMemory — there is no remote process-memory access: inaccessible or
// unusual processes return an error, and any anomaly degrades to an empty
// string rather than faulting. Safe to call across the whole process list.
func windowsProcessCmdline(pid int) string {
handle, err := syscall.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, uint32(pid))
if err != nil {
return ""
}
defer syscall.CloseHandle(handle)
const statusInfoLengthMismatch = 0xC0000004
bufLen := uint32(4096)
for attempt := 0; attempt < 2; attempt++ {
buf := make([]byte, bufLen)
var retLen uint32
status, _, _ := procNtQueryInfo.Call(
uintptr(handle),
uintptr(processCommandLineInformation),
uintptr(unsafe.Pointer(&buf[0])),
uintptr(bufLen),
uintptr(unsafe.Pointer(&retLen)),
)
// Buffer too small: grow to the reported size and retry once.
if uint32(status) == statusInfoLengthMismatch && retLen > bufLen && retLen <= 1<<20 {
bufLen = retLen
continue
}
if status != 0 {
return ""
}
// The buffer starts with a UNICODE_STRING whose Buffer points into the
// same buffer, just past the struct. Validate every offset before use.
us := (*unicodeString)(unsafe.Pointer(&buf[0]))
if us.Length == 0 || us.Buffer == 0 {
return ""
}
base := uintptr(unsafe.Pointer(&buf[0]))
if us.Buffer < base {
return ""
}
offset := us.Buffer - base
if offset+uintptr(us.Length) > uintptr(len(buf)) {
return ""
}
return syscall.UTF16ToString(unsafe.Slice((*uint16)(unsafe.Pointer(&buf[offset])), int(us.Length)/2))
}
return ""
}