refactor(windows): split Windows code into winapi (#575)

This commit is contained in:
Roger
2026-04-19 18:12:37 +08:00
committed by GitHub
parent 76e2615db2
commit ae1ec66ccb
21 changed files with 876 additions and 456 deletions
+12 -178
View File
@@ -6,68 +6,17 @@ import (
"fmt"
"os"
"strings"
"unsafe"
"golang.org/x/sys/windows"
)
const (
// systemExtendedHandleInformation is the information class for
// NtQuerySystemInformation that returns SYSTEM_HANDLE_INFORMATION_EX.
// This is the 64-bit safe version (class 64) — UniqueProcessId is ULONG_PTR
// instead of USHORT, avoiding PID truncation on 64-bit Windows.
systemExtendedHandleInformation = 64
statusInfoLengthMismatch = 0xC0000004
fileMapRead = 0x0004
pageReadonly = 0x02
fileTypeDisk = 0x0001
)
// systemHandleTableEntryInfoEx represents SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX.
// This is the extended version returned by SystemExtendedHandleInformation (class 64).
//
// Layout (64-bit Windows):
//
// PVOID Object; // 8 bytes
// ULONG_PTR UniqueProcessId; // 8 bytes
// ULONG_PTR HandleValue; // 8 bytes
// ULONG GrantedAccess; // 4 bytes
// USHORT CreatorBackTraceIndex; // 2 bytes
// USHORT ObjectTypeIndex; // 2 bytes
// ULONG HandleAttributes; // 4 bytes
// ULONG Reserved; // 4 bytes
// Total: 40 bytes on 64-bit
type systemHandleTableEntryInfoEx struct {
Object uintptr
UniqueProcessID uintptr // ULONG_PTR: safe for PID > 65535
HandleValue uintptr // ULONG_PTR: safe for large handle values
GrantedAccess uint32
CreatorBackTraceIndex uint16
ObjectTypeIndex uint16
HandleAttributes uint32
Reserved uint32
}
var (
ntdll = windows.NewLazySystemDLL("ntdll.dll")
procNtQuerySystemInformation = ntdll.NewProc("NtQuerySystemInformation")
kernel32 = windows.NewLazySystemDLL("kernel32.dll")
procGetFileType = kernel32.NewProc("GetFileType")
procGetFinalPathNameByHandleW = kernel32.NewProc("GetFinalPathNameByHandleW")
procCreateFileMappingW = kernel32.NewProc("CreateFileMappingW")
procMapViewOfFile = kernel32.NewProc("MapViewOfFile")
procUnmapViewOfFile = kernel32.NewProc("UnmapViewOfFile")
procGetFileSizeEx = kernel32.NewProc("GetFileSizeEx")
"github.com/moond4rk/hackbrowserdata/utils/winapi"
)
// copyLocked copies a file that is locked by another process (e.g., Chrome's
// Cookies database with PRAGMA locking_mode=EXCLUSIVE).
//
// Approach: DuplicateHandle + FileMapping
// 1. Enumerate all open file handles via NtQuerySystemInformation(SystemExtendedHandleInformation)
// 1. Enumerate all open file handles via NtQuerySystemInformation
// 2. Find the handle matching the target file path
// 3. Duplicate that handle into our process via DuplicateHandle
// 4. Read file content through memory-mapped I/O (CreateFileMapping + MapViewOfFile)
@@ -100,7 +49,7 @@ func findFileHandle(targetPath string) (windows.Handle, error) {
targetSuffix := extractStableSuffix(targetPath)
currentProcess := windows.CurrentProcess()
handles, err := querySystemHandles()
handles, err := winapi.QuerySystemHandles()
if err != nil {
return 0, err
}
@@ -133,14 +82,13 @@ func findFileHandle(targetPath string) (windows.Handle, error) {
}
// Verify it's a disk file (not a pipe, device, etc.)
fileType, _, _ := procGetFileType.Call(uintptr(dupHandle))
if fileType != fileTypeDisk {
if winapi.GetFileType(dupHandle) != winapi.FileTypeDisk {
_ = windows.CloseHandle(dupHandle)
continue
}
// Get the file path and check if it matches our target
name, err := getFinalPathName(dupHandle)
name, err := winapi.GetFinalPathName(dupHandle)
if err != nil {
_ = windows.CloseHandle(dupHandle)
continue
@@ -155,101 +103,15 @@ func findFileHandle(targetPath string) (windows.Handle, error) {
return 0, fmt.Errorf("no process has file open: %s", targetPath)
}
// querySystemHandles calls NtQuerySystemInformation with
// SystemExtendedHandleInformation (class 64) to enumerate all open handles.
func querySystemHandles() ([]systemHandleTableEntryInfoEx, error) {
bufSize := uint32(4 * 1024 * 1024) // start at 4 MB
for {
buf := make([]byte, bufSize)
var returnLength uint32
ret, _, _ := procNtQuerySystemInformation.Call(
systemExtendedHandleInformation,
uintptr(unsafe.Pointer(&buf[0])),
uintptr(bufSize),
uintptr(unsafe.Pointer(&returnLength)),
)
if ret == statusInfoLengthMismatch {
bufSize *= 2
if bufSize > 256*1024*1024 {
return nil, fmt.Errorf("handle info buffer exceeded 256 MB")
}
continue
}
if ret != 0 {
return nil, fmt.Errorf("NtQuerySystemInformation returned 0x%x", ret)
}
// Parse: first field is NumberOfHandles (ULONG_PTR), then array of entries
// On 64-bit: ULONG_PTR = 8 bytes
numberOfHandles := *(*uintptr)(unsafe.Pointer(&buf[0]))
if numberOfHandles == 0 {
return nil, nil
}
count := int(numberOfHandles)
// Entries start after NumberOfHandles + Reserved (both ULONG_PTR = 16 bytes total)
const headerSize = unsafe.Sizeof(uintptr(0)) * 2
entrySize := unsafe.Sizeof(systemHandleTableEntryInfoEx{})
// Validate buffer bounds
required := headerSize + uintptr(count)*entrySize
if required > uintptr(len(buf)) {
return nil, fmt.Errorf("buffer too small: need %d, have %d", required, len(buf))
}
entries := make([]systemHandleTableEntryInfoEx, count)
for i := 0; i < count; i++ {
src := unsafe.Pointer(uintptr(unsafe.Pointer(&buf[0])) + headerSize + uintptr(i)*entrySize)
entries[i] = *(*systemHandleTableEntryInfoEx)(src)
}
return entries, nil
}
}
// getFinalPathName returns the normalized file path for a file handle.
func getFinalPathName(handle windows.Handle) (string, error) {
size := 512
for {
buf := make([]uint16, size)
n, _, err := procGetFinalPathNameByHandleW.Call(
uintptr(handle),
uintptr(unsafe.Pointer(&buf[0])),
uintptr(len(buf)),
0, // FILE_NAME_NORMALIZED
)
if n == 0 {
return "", fmt.Errorf("GetFinalPathNameByHandle: %w", err)
}
if int(n) > len(buf) {
// Buffer too small, retry with required size
size = int(n)
continue
}
path := windows.UTF16ToString(buf[:n])
// Remove \\?\ prefix added by GetFinalPathNameByHandle
path = strings.TrimPrefix(path, `\\?\`)
return path, nil
}
}
// readFileContent reads file content from a duplicated handle.
// It uses FileMapping first (CreateFileMapping + MapViewOfFile), which reads
// from the OS kernel's file cache — this includes WAL data that Chrome has
// written but not yet checkpointed to the main file. Falls back to ReadFile
// if FileMapping fails.
func readFileContent(handle windows.Handle) ([]byte, error) {
// Get file size
var fileSize int64
ret, _, sizeErr := procGetFileSizeEx.Call(
uintptr(handle),
uintptr(unsafe.Pointer(&fileSize)),
)
if ret == 0 {
return nil, fmt.Errorf("GetFileSizeEx: %w", sizeErr)
fileSize, err := winapi.GetFileSizeEx(handle)
if err != nil {
return nil, err
}
if fileSize == 0 {
return nil, fmt.Errorf("file is empty")
@@ -258,12 +120,13 @@ func readFileContent(handle windows.Handle) ([]byte, error) {
size := int(fileSize)
// Try FileMapping first — reads from kernel file cache, includes WAL data
if data, err := readViaFileMapping(handle, size); err == nil {
if data, err := winapi.MapFile(handle, size); err == nil {
return data, nil
}
// FileMapping failed, fall back to ReadFile
// Seek to beginning first — the handle's file pointer may be at an arbitrary position
// FileMapping failed, fall back to ReadFile.
// Seek to beginning first — the handle's file pointer may be at an
// arbitrary position.
if _, err := windows.Seek(handle, 0, 0); err != nil {
return nil, fmt.Errorf("seek to start: %w", err)
}
@@ -275,35 +138,6 @@ func readFileContent(handle windows.Handle) ([]byte, error) {
return data[:bytesRead], nil
}
// readViaFileMapping reads file content using CreateFileMapping + MapViewOfFile.
func readViaFileMapping(handle windows.Handle, size int) ([]byte, error) {
mapping, _, err := procCreateFileMappingW.Call(
uintptr(handle),
0, pageReadonly,
0, 0, 0,
)
if mapping == 0 {
return nil, fmt.Errorf("CreateFileMapping: %w", err)
}
defer windows.CloseHandle(windows.Handle(mapping))
viewPtr, _, err := procMapViewOfFile.Call(
mapping, fileMapRead,
0, 0, 0,
)
if viewPtr == 0 {
return nil, fmt.Errorf("MapViewOfFile: %w", err)
}
defer procUnmapViewOfFile.Call(viewPtr)
// viewPtr is a valid pointer from MapViewOfFile syscall.
// go vet flags this as "possible misuse of unsafe.Pointer" but it's
// correct usage for Windows memory-mapped I/O.
data := make([]byte, size)
copy(data, (*[1 << 30]byte)(unsafe.Pointer(viewPtr))[:size]) //nolint:govet
return data, nil
}
// extractStableSuffix extracts a path suffix that is stable across short/long
// path name variations. It finds "AppData" in the path and returns everything
// after "AppData\Local\" or "AppData\Roaming\" in lowercase.