├── .gitattributes ├── CODEOWNERS ├── internal ├── interop │ ├── doc.go │ ├── interop.go │ └── zsyscall_windows.go ├── computestorage │ ├── doc.go │ ├── computestorage.go │ └── zsyscall_windows.go ├── fs │ ├── doc.go │ ├── security.go │ ├── fs_test.go │ └── zsyscall_windows.go ├── socket │ ├── rawaddr.go │ ├── zsyscall_windows.go │ └── socket.go └── stringbuffer │ ├── wstring_test.go │ └── wstring.go ├── pkg ├── fs │ ├── doc.go │ ├── fs_windows.go │ ├── fs_windows_test.go │ ├── resolve.go │ └── resolve_test.go ├── etw │ ├── sample │ │ ├── main_other.go │ │ └── main_windows.go │ ├── newprovider_unsupported.go │ ├── ptr64_64.go │ ├── ptr64_32.go │ ├── doc.go │ ├── eventdatadescriptor.go │ ├── opcode_string.go │ ├── level_string.go │ ├── provider_test.go │ ├── providerglobal.go │ ├── syscall.go │ ├── wrapper_64.go │ ├── wrapper_32.go │ ├── eventopt.go │ ├── eventdata.go │ ├── newprovider.go │ ├── eventdescriptor.go │ ├── zsyscall_windows.go │ └── eventmetadata.go ├── guid │ ├── guid_windows.go │ ├── guid_nonwindows.go │ ├── variant_string.go │ ├── guid.go │ └── guid_test.go ├── process │ ├── syscall.go │ ├── zsyscall_windows.go │ └── process.go ├── security │ ├── syscall_windows.go │ ├── zsyscall_windows.go │ ├── grantvmgroupaccess_test.go │ └── grantvmgroupaccess.go ├── etwlogrus │ ├── HookTest.wprp │ ├── opts.go │ ├── hook_test.go │ └── hook.go └── bindfilter │ └── zsyscall_windows.go ├── wim ├── validate │ ├── main_other.go │ └── main_windows.go └── decompress.go ├── tools ├── etw-provider-gen │ ├── main_others.go │ └── main_windows.go ├── tools.go └── mkwinsyscall │ └── doc.go ├── backuptar ├── doc.go ├── strconv.go └── tar_test.go ├── .gitignore ├── syscall.go ├── go.mod ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── privileges_test.go ├── doc.go ├── LICENSE ├── sd_test.go ├── vhd ├── vhd_test.go └── zvhd_windows.go ├── go.sum ├── ea_test.go ├── SECURITY.md ├── hvsock_go118_test.go ├── ea.go ├── README.md ├── fileinfo.go ├── reparse.go ├── sd.go ├── .golangci.yml ├── backup_test.go ├── fileinfo_test.go └── privilege.go /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @microsoft/containerplat 2 | -------------------------------------------------------------------------------- /internal/interop/doc.go: -------------------------------------------------------------------------------- 1 | package interop 2 | -------------------------------------------------------------------------------- /internal/computestorage/doc.go: -------------------------------------------------------------------------------- 1 | package computestorage 2 | -------------------------------------------------------------------------------- /pkg/fs/doc.go: -------------------------------------------------------------------------------- 1 | // This package contains Win32 filesystem functionality. 2 | package fs 3 | -------------------------------------------------------------------------------- /internal/fs/doc.go: -------------------------------------------------------------------------------- 1 | // This package contains Win32 filesystem functionality. 2 | package fs 3 | -------------------------------------------------------------------------------- /pkg/etw/sample/main_other.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package main 4 | 5 | func main() {} 6 | -------------------------------------------------------------------------------- /wim/validate/main_other.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package main 4 | 5 | func main() {} 6 | -------------------------------------------------------------------------------- /tools/etw-provider-gen/main_others.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package main 4 | 5 | func main() {} 6 | -------------------------------------------------------------------------------- /backuptar/doc.go: -------------------------------------------------------------------------------- 1 | // This file only exists to allow go get on non-Windows platforms. 2 | 3 | package backuptar 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | 3 | *.exe 4 | 5 | # testing 6 | testdata 7 | 8 | # go workspaces 9 | go.work 10 | go.work.sum 11 | -------------------------------------------------------------------------------- /syscall.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package winio 4 | 5 | //go:generate go run github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go ./*.go 6 | -------------------------------------------------------------------------------- /pkg/etw/newprovider_unsupported.go: -------------------------------------------------------------------------------- 1 | //go:build windows && arm 2 | 3 | package etw 4 | 5 | // NewProviderWithID returns a nil provider on unsupported platforms. 6 | func NewProviderWithOptions(name string, options ...ProviderOpt) (provider *Provider, err error) { 7 | return nil, nil 8 | } 9 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Microsoft/go-winio 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.8 6 | 7 | require ( 8 | github.com/sirupsen/logrus v1.9.3 9 | golang.org/x/sys v0.35.0 10 | golang.org/x/tools v0.36.0 11 | ) 12 | 13 | require ( 14 | golang.org/x/mod v0.27.0 // indirect 15 | golang.org/x/sync v0.16.0 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /pkg/etw/ptr64_64.go: -------------------------------------------------------------------------------- 1 | //go:build windows && (amd64 || arm64) 2 | 3 | package etw 4 | 5 | import ( 6 | "unsafe" 7 | ) 8 | 9 | // byteptr64 defines a struct containing a pointer. The struct is guaranteed to 10 | // be 64 bits, regardless of the actual size of a pointer on the platform. This 11 | // is intended for use with certain Windows APIs that expect a pointer as a 12 | // ULONGLONG. 13 | type ptr64 struct { 14 | ptr unsafe.Pointer 15 | } 16 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # reference: 2 | # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: "gomod" 7 | directory: "/" 8 | schedule: 9 | interval: "weekly" 10 | day: "sunday" 11 | 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: "daily" 16 | -------------------------------------------------------------------------------- /pkg/etw/ptr64_32.go: -------------------------------------------------------------------------------- 1 | //go:build windows && (386 || arm) 2 | 3 | package etw 4 | 5 | import ( 6 | "unsafe" 7 | ) 8 | 9 | // byteptr64 defines a struct containing a pointer. The struct is guaranteed to 10 | // be 64 bits, regardless of the actual size of a pointer on the platform. This 11 | // is intended for use with certain Windows APIs that expect a pointer as a 12 | // ULONGLONG. 13 | type ptr64 struct { 14 | ptr unsafe.Pointer 15 | _ uint32 16 | } 17 | -------------------------------------------------------------------------------- /pkg/guid/guid_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package guid 4 | 5 | import "golang.org/x/sys/windows" 6 | 7 | // GUID represents a GUID/UUID. It has the same structure as 8 | // golang.org/x/sys/windows.GUID so that it can be used with functions expecting 9 | // that type. It is defined as its own type so that stringification and 10 | // marshaling can be supported. The representation matches that used by native 11 | // Windows code. 12 | type GUID windows.GUID 13 | -------------------------------------------------------------------------------- /pkg/etw/doc.go: -------------------------------------------------------------------------------- 1 | // Package etw provides support for TraceLogging-based ETW (Event Tracing 2 | // for Windows). TraceLogging is a format of ETW events that are self-describing 3 | // (the event contains information on its own schema). This allows them to be 4 | // decoded without needing a separate manifest with event information. The 5 | // implementation here is based on the information found in 6 | // TraceLoggingProvider.h in the Windows SDK, which implements TraceLogging as a 7 | // set of C macros. 8 | package etw 9 | -------------------------------------------------------------------------------- /pkg/guid/guid_nonwindows.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package guid 4 | 5 | // GUID represents a GUID/UUID. It has the same structure as 6 | // golang.org/x/sys/windows.GUID so that it can be used with functions expecting 7 | // that type. It is defined as its own type as that is only available to builds 8 | // targeted at `windows`. The representation matches that used by native Windows 9 | // code. 10 | type GUID struct { 11 | Data1 uint32 12 | Data2 uint16 13 | Data3 uint16 14 | Data4 [8]byte 15 | } 16 | -------------------------------------------------------------------------------- /internal/fs/security.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | // https://learn.microsoft.com/en-us/windows/win32/api/winnt/ne-winnt-security_impersonation_level 4 | type SecurityImpersonationLevel int32 // C default enums underlying type is `int`, which is Go `int32` 5 | 6 | // Impersonation levels 7 | const ( 8 | SecurityAnonymous SecurityImpersonationLevel = 0 9 | SecurityIdentification SecurityImpersonationLevel = 1 10 | SecurityImpersonation SecurityImpersonationLevel = 2 11 | SecurityDelegation SecurityImpersonationLevel = 3 12 | ) 13 | -------------------------------------------------------------------------------- /privileges_test.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package winio 4 | 5 | import ( 6 | "errors" 7 | "testing" 8 | ) 9 | 10 | func TestRunWithUnavailablePrivilege(t *testing.T) { 11 | err := RunWithPrivilege("SeCreateTokenPrivilege", func() error { return nil }) 12 | var perr *PrivilegeError 13 | if !errors.As(err, &perr) { 14 | t.Fatal("expected PrivilegeError") 15 | } 16 | } 17 | 18 | func TestRunWithPrivileges(t *testing.T) { 19 | err := RunWithPrivilege("SeShutdownPrivilege", func() error { return nil }) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | 3 | // This package contains imports to various tools used (eg, via `//go:generate`) within this repo. 4 | // 5 | // Calls to `go run ` (or `//go:generate go run `) for go executables 6 | // included here will use the version specified in `go.mod` and build the executable from vendored code. 7 | // 8 | // Based on golang [guidance]. 9 | // 10 | // [guidance]: https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module 11 | package tools 12 | 13 | import _ "golang.org/x/tools/cmd/stringer" 14 | -------------------------------------------------------------------------------- /pkg/process/syscall.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package process 4 | 5 | import ( 6 | "golang.org/x/sys/windows" 7 | ) 8 | 9 | //go:generate go run github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go syscall.go 10 | 11 | //sys enumProcesses(pids *uint32, bufferSize uint32, retBufferSize *uint32) (err error) = kernel32.K32EnumProcesses 12 | //sys getProcessMemoryInfo(process handle, memCounters *ProcessMemoryCountersEx, size uint32) (err error) = kernel32.K32GetProcessMemoryInfo 13 | //sys queryFullProcessImageName(process handle, flags uint32, buffer *uint16, bufferSize *uint32) (err error) = kernel32.QueryFullProcessImageNameW 14 | 15 | type handle = windows.Handle 16 | -------------------------------------------------------------------------------- /tools/etw-provider-gen/main_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package main 4 | 5 | import ( 6 | "flag" 7 | "fmt" 8 | "os" 9 | 10 | "github.com/Microsoft/go-winio/pkg/etw" 11 | ) 12 | 13 | func main() { 14 | var pn = flag.String("provider-name", "", "The human readable ETW provider name to be converted into GUID format") 15 | flag.Parse() 16 | if pn == nil || *pn == "" { 17 | fmt.Fprint(os.Stderr, "--provider-name is required") 18 | os.Exit(1) 19 | } 20 | p, err := etw.NewProvider(*pn, nil) 21 | if err != nil { 22 | fmt.Fprintf(os.Stderr, "failed to convert provider-name: '%s' with err: '%s", *pn, err) 23 | os.Exit(1) 24 | } 25 | defer p.Close() 26 | fmt.Fprintf(os.Stdout, "%s", p) 27 | } 28 | -------------------------------------------------------------------------------- /pkg/security/syscall_windows.go: -------------------------------------------------------------------------------- 1 | package security 2 | 3 | //go:generate go run github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go syscall_windows.go 4 | 5 | //sys getSecurityInfo(handle windows.Handle, objectType uint32, si uint32, ppsidOwner **uintptr, ppsidGroup **uintptr, ppDacl *uintptr, ppSacl *uintptr, ppSecurityDescriptor *uintptr) (win32err error) = advapi32.GetSecurityInfo 6 | //sys setSecurityInfo(handle windows.Handle, objectType uint32, si uint32, psidOwner uintptr, psidGroup uintptr, pDacl uintptr, pSacl uintptr) (win32err error) = advapi32.SetSecurityInfo 7 | //sys setEntriesInAcl(count uintptr, pListOfEEs uintptr, oldAcl uintptr, newAcl *uintptr) (win32err error) = advapi32.SetEntriesInAclW 8 | -------------------------------------------------------------------------------- /internal/interop/interop.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package interop 4 | 5 | import ( 6 | "syscall" 7 | "unsafe" 8 | ) 9 | 10 | //go:generate go run github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go interop.go 11 | 12 | //sys coTaskMemFree(buffer unsafe.Pointer) = api_ms_win_core_com_l1_1_0.CoTaskMemFree 13 | 14 | func ConvertAndFreeCoTaskMemString(buffer *uint16) string { 15 | str := syscall.UTF16ToString((*[1 << 29]uint16)(unsafe.Pointer(buffer))[:]) 16 | coTaskMemFree(unsafe.Pointer(buffer)) 17 | return str 18 | } 19 | 20 | func Win32FromHresult(hr uintptr) syscall.Errno { 21 | if hr&0x1fff0000 == 0x00070000 { 22 | return syscall.Errno(hr & 0xffff) 23 | } 24 | return syscall.Errno(hr) 25 | } 26 | -------------------------------------------------------------------------------- /pkg/etwlogrus/HookTest.wprp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /pkg/etw/eventdatadescriptor.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package etw 4 | 5 | import ( 6 | "unsafe" 7 | ) 8 | 9 | type eventDataDescriptorType uint8 10 | 11 | const ( 12 | eventDataDescriptorTypeUserData eventDataDescriptorType = iota 13 | eventDataDescriptorTypeEventMetadata 14 | eventDataDescriptorTypeProviderMetadata 15 | ) 16 | 17 | type eventDataDescriptor struct { 18 | ptr ptr64 19 | size uint32 20 | dataType eventDataDescriptorType 21 | _ uint8 22 | _ uint16 23 | } 24 | 25 | func newEventDataDescriptor(dataType eventDataDescriptorType, buffer []byte) eventDataDescriptor { 26 | return eventDataDescriptor{ 27 | ptr: ptr64{ptr: unsafe.Pointer(&buffer[0])}, 28 | size: uint32(len(buffer)), 29 | dataType: dataType, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /internal/socket/rawaddr.go: -------------------------------------------------------------------------------- 1 | package socket 2 | 3 | import ( 4 | "unsafe" 5 | ) 6 | 7 | // RawSockaddr allows structs to be used with [Bind] and [ConnectEx]. The 8 | // struct must meet the Win32 sockaddr requirements specified here: 9 | // https://docs.microsoft.com/en-us/windows/win32/winsock/sockaddr-2 10 | // 11 | // Specifically, the struct size must be least larger than an int16 (unsigned short) 12 | // for the address family. 13 | type RawSockaddr interface { 14 | // Sockaddr returns a pointer to the RawSockaddr and its struct size, allowing 15 | // for the RawSockaddr's data to be overwritten by syscalls (if necessary). 16 | // 17 | // It is the callers responsibility to validate that the values are valid; invalid 18 | // pointers or size can cause a panic. 19 | Sockaddr() (unsafe.Pointer, int32, error) 20 | } 21 | -------------------------------------------------------------------------------- /pkg/etw/opcode_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=Opcode -trimprefix=Opcode"; DO NOT EDIT. 2 | 3 | package etw 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[OpcodeInfo-0] 12 | _ = x[OpcodeStart-1] 13 | _ = x[OpcodeStop-2] 14 | _ = x[OpcodeDCStart-3] 15 | _ = x[OpcodeDCStop-4] 16 | } 17 | 18 | const _Opcode_name = "InfoStartStopDCStartDCStop" 19 | 20 | var _Opcode_index = [...]uint8{0, 4, 9, 13, 20, 26} 21 | 22 | func (i Opcode) String() string { 23 | if i >= Opcode(len(_Opcode_index)-1) { 24 | return "Opcode(" + strconv.FormatInt(int64(i), 10) + ")" 25 | } 26 | return _Opcode_name[_Opcode_index[i]:_Opcode_index[i+1]] 27 | } 28 | -------------------------------------------------------------------------------- /pkg/etw/level_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=Level -trimprefix=Level"; DO NOT EDIT. 2 | 3 | package etw 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[LevelAlways-0] 12 | _ = x[LevelCritical-1] 13 | _ = x[LevelError-2] 14 | _ = x[LevelWarning-3] 15 | _ = x[LevelInfo-4] 16 | _ = x[LevelVerbose-5] 17 | } 18 | 19 | const _Level_name = "AlwaysCriticalErrorWarningInfoVerbose" 20 | 21 | var _Level_index = [...]uint8{0, 6, 14, 19, 26, 30, 37} 22 | 23 | func (i Level) String() string { 24 | if i >= Level(len(_Level_index)-1) { 25 | return "Level(" + strconv.FormatInt(int64(i), 10) + ")" 26 | } 27 | return _Level_name[_Level_index[i]:_Level_index[i+1]] 28 | } 29 | -------------------------------------------------------------------------------- /pkg/guid/variant_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=Variant -trimprefix=Variant -linecomment"; DO NOT EDIT. 2 | 3 | package guid 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[VariantUnknown-0] 12 | _ = x[VariantNCS-1] 13 | _ = x[VariantRFC4122-2] 14 | _ = x[VariantMicrosoft-3] 15 | _ = x[VariantFuture-4] 16 | } 17 | 18 | const _Variant_name = "UnknownNCSRFC 4122MicrosoftFuture" 19 | 20 | var _Variant_index = [...]uint8{0, 7, 10, 18, 27, 33} 21 | 22 | func (i Variant) String() string { 23 | if i >= Variant(len(_Variant_index)-1) { 24 | return "Variant(" + strconv.FormatInt(int64(i), 10) + ")" 25 | } 26 | return _Variant_name[_Variant_index[i]:_Variant_index[i+1]] 27 | } 28 | -------------------------------------------------------------------------------- /wim/validate/main_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package main 4 | 5 | import ( 6 | "flag" 7 | "fmt" 8 | "os" 9 | 10 | "github.com/Microsoft/go-winio/wim" 11 | ) 12 | 13 | func main() { 14 | flag.Parse() 15 | f, err := os.Open(flag.Arg(0)) 16 | if err != nil { 17 | panic(err) 18 | } 19 | 20 | w, err := wim.NewReader(f) 21 | if err != nil { 22 | panic(err) 23 | } 24 | 25 | fmt.Printf("%#v\n%#v\n", w.Image[0], w.Image[0].Windows) 26 | 27 | dir, err := w.Image[0].Open() 28 | if err != nil { 29 | panic(err) 30 | } 31 | 32 | err = recur(dir) 33 | if err != nil { 34 | panic(err) 35 | } 36 | } 37 | 38 | func recur(d *wim.File) error { 39 | files, err := d.Readdir() 40 | if err != nil { 41 | return fmt.Errorf("%s: %w", d.Name, err) 42 | } 43 | for _, f := range files { 44 | if f.IsDir() { 45 | err = recur(f) 46 | if err != nil { 47 | return fmt.Errorf("%s: %w", f.Name, err) 48 | } 49 | } 50 | } 51 | return nil 52 | } 53 | -------------------------------------------------------------------------------- /pkg/fs/fs_windows.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "errors" 5 | "path/filepath" 6 | 7 | "golang.org/x/sys/windows" 8 | 9 | "github.com/Microsoft/go-winio/internal/stringbuffer" 10 | ) 11 | 12 | var ( 13 | // ErrInvalidPath is returned when the location of a file path doesn't begin with a driver letter. 14 | ErrInvalidPath = errors.New("the path provided to GetFileSystemType must start with a drive letter") 15 | ) 16 | 17 | // GetFileSystemType obtains the type of a file system through GetVolumeInformation. 18 | // 19 | // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getvolumeinformationw 20 | func GetFileSystemType(path string) (fsType string, err error) { 21 | drive := filepath.VolumeName(path) 22 | if len(drive) != 2 { 23 | return "", ErrInvalidPath 24 | } 25 | 26 | buf := stringbuffer.NewWString() 27 | defer buf.Free() 28 | 29 | drive += `\` 30 | err = windows.GetVolumeInformation(windows.StringToUTF16Ptr(drive), nil, 0, nil, nil, nil, buf.Pointer(), buf.Cap()) 31 | return buf.String(), err 32 | } 33 | -------------------------------------------------------------------------------- /pkg/etw/provider_test.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package etw 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/Microsoft/go-winio/pkg/guid" 9 | ) 10 | 11 | func mustGUIDFromString(t *testing.T, s string) guid.GUID { 12 | t.Helper() 13 | 14 | g, err := guid.FromString(s) 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | return g 19 | } 20 | 21 | func Test_ProviderIDFromName(t *testing.T) { 22 | type testCase struct { 23 | name string 24 | g guid.GUID 25 | } 26 | testCases := []testCase{ 27 | {"wincni", mustGUIDFromString(t, "c822b598-f4cc-5a72-7933-ce2a816d033f")}, 28 | {"Moby", mustGUIDFromString(t, "6996f090-c5de-5082-a81e-5841acc3a635")}, 29 | {"ContainerD", mustGUIDFromString(t, "2acb92c0-eb9b-571a-69cf-8f3410f383ad")}, 30 | {"Microsoft.Virtualization.RunHCS", mustGUIDFromString(t, "0B52781F-B24D-5685-DDF6-69830ED40EC3")}, 31 | } 32 | for _, tc := range testCases { 33 | g := providerIDFromName(tc.name) 34 | if g != tc.g { 35 | t.Fatalf("Incorrect provider GUID.\nExpected: %s\nActual: %s", tc.g, g) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /internal/fs/fs_test.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package fs 4 | 5 | import ( 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | "testing" 10 | 11 | "golang.org/x/sys/windows" 12 | ) 13 | 14 | func Test_GetFinalPathNameByHandle(t *testing.T) { 15 | d := t.TempDir() 16 | // open f via a relative path 17 | name := t.Name() + ".txt" 18 | fullPath := filepath.Join(d, name) 19 | 20 | w, err := os.Getwd() 21 | if err != nil { 22 | t.Fatalf("could not get working directory: %v", err) 23 | } 24 | if err := os.Chdir(d); err != nil { 25 | t.Fatalf("could not chdir to %s: %v", d, err) 26 | } 27 | defer os.Chdir(w) //nolint:errcheck 28 | 29 | f, err := os.Create(name) 30 | if err != nil { 31 | t.Fatalf("could not open %s: %v", fullPath, err) 32 | } 33 | defer f.Close() 34 | 35 | path, err := GetFinalPathNameByHandle(windows.Handle(f.Fd()), GetFinalPathDefaultFlag) 36 | if err != nil { 37 | t.Fatalf("could not get final path for %s: %v", fullPath, err) 38 | } 39 | if strings.EqualFold(fullPath, path) { 40 | t.Fatalf("expected %s, got %s", fullPath, path) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // This package provides utilities for efficiently performing Win32 IO operations in Go. 2 | // Currently, this package is provides support for genreal IO and management of 3 | // - named pipes 4 | // - files 5 | // - [Hyper-V sockets] 6 | // 7 | // This code is similar to Go's [net] package, and uses IO completion ports to avoid 8 | // blocking IO on system threads, allowing Go to reuse the thread to schedule other goroutines. 9 | // 10 | // This limits support to Windows Vista and newer operating systems. 11 | // 12 | // Additionally, this package provides support for: 13 | // - creating and managing GUIDs 14 | // - writing to [ETW] 15 | // - opening and manageing VHDs 16 | // - parsing [Windows Image files] 17 | // - auto-generating Win32 API code 18 | // 19 | // [Hyper-V sockets]: https://docs.microsoft.com/en-us/virtualization/hyper-v-on-windows/user-guide/make-integration-service 20 | // [ETW]: https://docs.microsoft.com/en-us/windows-hardware/drivers/devtest/event-tracing-for-windows--etw- 21 | // [Windows Image files]: https://docs.microsoft.com/en-us/windows-hardware/manufacture/desktop/work-with-windows-images 22 | package winio 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Microsoft 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /internal/stringbuffer/wstring_test.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package stringbuffer 4 | 5 | import "testing" 6 | 7 | func Test_BufferCapacity(t *testing.T) { 8 | b := NewWString() 9 | 10 | c := b.Cap() 11 | if c < MinWStringCap { 12 | t.Fatalf("expected capacity >= %d, got %d", MinWStringCap, c) 13 | } 14 | 15 | if l := len(b.b); l != int(c) { 16 | t.Fatalf("buffer length (%d) and capacity (%d) mismatch", l, c) 17 | } 18 | 19 | n := uint32(1.5 * MinWStringCap) 20 | nn := b.ResizeTo(n) 21 | if len(b.b) != int(nn) { 22 | t.Fatalf("resized buffer should be %d, was %d", nn, len(b.b)) 23 | } 24 | if n > nn { 25 | t.Fatalf("resized to a value smaller than requested") 26 | } 27 | } 28 | 29 | func Test_BufferFree(t *testing.T) { 30 | // make sure free-ing doesn't set pooled buffer to nil as well 31 | for i := 0; i < 256; i++ { 32 | // try allocating and freeing repeatedly since pool does not guarantee item reuse 33 | b := NewWString() 34 | b.Free() 35 | if b.b != nil { 36 | t.Fatalf("freed buffer is not nil") 37 | } 38 | 39 | b = NewWString() 40 | c := b.Cap() 41 | if c < MinWStringCap { 42 | t.Fatalf("expected capacity >= %d, got %d", MinWStringCap, c) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /internal/interop/zsyscall_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | // Code generated by 'go generate' using "github.com/Microsoft/go-winio/tools/mkwinsyscall"; DO NOT EDIT. 4 | 5 | package interop 6 | 7 | import ( 8 | "syscall" 9 | "unsafe" 10 | 11 | "golang.org/x/sys/windows" 12 | ) 13 | 14 | var _ unsafe.Pointer 15 | 16 | // Do the interface allocations only once for common 17 | // Errno values. 18 | const ( 19 | errnoERROR_IO_PENDING = 997 20 | ) 21 | 22 | var ( 23 | errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING) 24 | errERROR_EINVAL error = syscall.EINVAL 25 | ) 26 | 27 | // errnoErr returns common boxed Errno values, to prevent 28 | // allocations at runtime. 29 | func errnoErr(e syscall.Errno) error { 30 | switch e { 31 | case 0: 32 | return errERROR_EINVAL 33 | case errnoERROR_IO_PENDING: 34 | return errERROR_IO_PENDING 35 | } 36 | return e 37 | } 38 | 39 | var ( 40 | modapi_ms_win_core_com_l1_1_0 = windows.NewLazySystemDLL("api-ms-win-core-com-l1-1-0.dll") 41 | 42 | procCoTaskMemFree = modapi_ms_win_core_com_l1_1_0.NewProc("CoTaskMemFree") 43 | ) 44 | 45 | func coTaskMemFree(buffer unsafe.Pointer) { 46 | syscall.SyscallN(procCoTaskMemFree.Addr(), uintptr(buffer)) 47 | return 48 | } 49 | -------------------------------------------------------------------------------- /pkg/etw/providerglobal.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package etw 4 | 5 | import ( 6 | "sync" 7 | ) 8 | 9 | // Because the provider callback function needs to be able to access the 10 | // provider data when it is invoked by ETW, we need to keep provider data stored 11 | // in a global map based on an index. The index is passed as the callback 12 | // context to ETW. 13 | type providerMap struct { 14 | m map[uint]*Provider 15 | i uint 16 | lock sync.Mutex 17 | } 18 | 19 | var providers = providerMap{ 20 | m: make(map[uint]*Provider), 21 | } 22 | 23 | func (p *providerMap) newProvider() *Provider { 24 | p.lock.Lock() 25 | defer p.lock.Unlock() 26 | 27 | i := p.i 28 | p.i++ 29 | 30 | provider := &Provider{ 31 | index: i, 32 | } 33 | 34 | p.m[i] = provider 35 | return provider 36 | } 37 | 38 | func (p *providerMap) removeProvider(provider *Provider) { 39 | p.lock.Lock() 40 | defer p.lock.Unlock() 41 | 42 | delete(p.m, provider.index) 43 | } 44 | 45 | func (p *providerMap) getProvider(index uint) *Provider { 46 | p.lock.Lock() 47 | defer p.lock.Unlock() 48 | 49 | return p.m[index] 50 | } 51 | 52 | //todo: combine these into struct, so that "globalProviderCallback" is guaranteed to be initialized through method access 53 | 54 | var providerCallbackOnce sync.Once 55 | var globalProviderCallback uintptr 56 | -------------------------------------------------------------------------------- /pkg/fs/fs_windows_test.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | func TestGetFSTypeOfKnownDrive(t *testing.T) { 10 | fsType, err := GetFileSystemType("C:\\") 11 | if err != nil { 12 | t.Fatal(err) 13 | } 14 | 15 | if fsType == "" { 16 | t.Fatal("No filesystem type name returned") 17 | } 18 | } 19 | 20 | func TestGetFSTypeOfInvalidPath(t *testing.T) { 21 | // [filepath.VolumeName] doesn't mandate that the drive letters matches [a-zA-Z]. 22 | // Instead, use non-character drive. 23 | _, err := GetFileSystemType(`No:\`) 24 | if !errors.Is(err, ErrInvalidPath) { 25 | t.Fatalf("Expected `ErrInvalidPath`, got %v", err) 26 | } 27 | } 28 | 29 | func TestGetFSTypeOfValidButAbsentDrive(t *testing.T) { 30 | drive := "" 31 | for _, letter := range "abcdefghijklmnopqrstuvwxyz" { 32 | possibleDrive := string(letter) + ":\\" 33 | if _, err := os.Stat(possibleDrive); os.IsNotExist(err) { 34 | drive = possibleDrive 35 | break 36 | } 37 | } 38 | if drive == "" { 39 | t.Skip("Every possible drive exists") 40 | } 41 | 42 | _, err := GetFileSystemType(drive) 43 | if err == nil { 44 | t.Fatalf("GetFileSystemType %s unexpectedly succeeded", drive) 45 | } 46 | if !os.IsNotExist(err) { 47 | t.Fatalf("GetFileSystemType %s failed with %v, expected 'ErrNotExist' or similar", drive, err) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /sd_test.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package winio 4 | 5 | import ( 6 | "errors" 7 | "testing" 8 | 9 | "golang.org/x/sys/windows" 10 | ) 11 | 12 | func TestLookupInvalidSid(t *testing.T) { 13 | _, err := LookupSidByName(".\\weoifjdsklfj") 14 | var aerr *AccountLookupError 15 | if !errors.As(err, &aerr) || !errors.Is(err, windows.ERROR_NONE_MAPPED) { 16 | t.Fatalf("expected AccountLookupError with ERROR_NONE_MAPPED, got %s", err) 17 | } 18 | } 19 | 20 | func TestLookupInvalidName(t *testing.T) { 21 | _, err := LookupNameBySid("notasid") 22 | var aerr *AccountLookupError 23 | if !errors.As(err, &aerr) || !errors.Is(aerr.Err, windows.ERROR_INVALID_SID) { 24 | t.Fatalf("expected AccountLookupError with ERROR_INVALID_SID got %s", err) 25 | } 26 | } 27 | 28 | func TestLookupValidSid(t *testing.T) { 29 | everyone := "S-1-1-0" 30 | name, err := LookupNameBySid(everyone) 31 | if err != nil { 32 | t.Fatalf("expected a valid account name, got %v", err) 33 | } 34 | 35 | sid, err := LookupSidByName(name) 36 | if err != nil || sid != everyone { 37 | t.Fatalf("expected %s, got %s, %s", everyone, sid, err) 38 | } 39 | } 40 | 41 | func TestLookupEmptyNameFails(t *testing.T) { 42 | _, err := LookupSidByName("") 43 | var aerr *AccountLookupError 44 | if !errors.As(err, &aerr) || !errors.Is(aerr.Err, windows.ERROR_NONE_MAPPED) { 45 | t.Fatalf("expected AccountLookupError with ERROR_NONE_MAPPED, got %s", err) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /pkg/etw/syscall.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package etw 4 | 5 | //go:generate go run github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go syscall.go 6 | 7 | //sys eventRegister(providerId *windows.GUID, callback uintptr, callbackContext uintptr, providerHandle *providerHandle) (win32err error) = advapi32.EventRegister 8 | 9 | //sys eventUnregister_64(providerHandle providerHandle) (win32err error) = advapi32.EventUnregister 10 | //sys eventWriteTransfer_64(providerHandle providerHandle, descriptor *eventDescriptor, activityID *windows.GUID, relatedActivityID *windows.GUID, dataDescriptorCount uint32, dataDescriptors *eventDataDescriptor) (win32err error) = advapi32.EventWriteTransfer 11 | //sys eventSetInformation_64(providerHandle providerHandle, class eventInfoClass, information uintptr, length uint32) (win32err error) = advapi32.EventSetInformation 12 | 13 | //sys eventUnregister_32(providerHandle_low uint32, providerHandle_high uint32) (win32err error) = advapi32.EventUnregister 14 | //sys eventWriteTransfer_32(providerHandle_low uint32, providerHandle_high uint32, descriptor *eventDescriptor, activityID *windows.GUID, relatedActivityID *windows.GUID, dataDescriptorCount uint32, dataDescriptors *eventDataDescriptor) (win32err error) = advapi32.EventWriteTransfer 15 | //sys eventSetInformation_32(providerHandle_low uint32, providerHandle_high uint32, class eventInfoClass, information uintptr, length uint32) (win32err error) = advapi32.EventSetInformation 16 | -------------------------------------------------------------------------------- /pkg/etwlogrus/opts.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package etwlogrus 4 | 5 | import ( 6 | "github.com/sirupsen/logrus" 7 | 8 | "github.com/Microsoft/go-winio/pkg/etw" 9 | ) 10 | 11 | // etw provider 12 | 13 | // WithNewETWProvider registers a new ETW provider and sets the hook to log using it. 14 | // The provider will be closed when the hook is closed. 15 | func WithNewETWProvider(n string) HookOpt { 16 | return func(h *Hook) error { 17 | provider, err := etw.NewProvider(n, nil) 18 | if err != nil { 19 | return err 20 | } 21 | 22 | h.provider = provider 23 | h.closeProvider = true 24 | return nil 25 | } 26 | } 27 | 28 | // WithExistingETWProvider configures the hook to use an existing ETW provider. 29 | // The provider will not be closed when the hook is closed. 30 | func WithExistingETWProvider(p *etw.Provider) HookOpt { 31 | return func(h *Hook) error { 32 | h.provider = p 33 | h.closeProvider = false 34 | return nil 35 | } 36 | } 37 | 38 | // WithGetName sets the ETW EventName of an event to the value returned by f 39 | // If the name is empty, the default event name will be used. 40 | func WithGetName(f func(*logrus.Entry) string) HookOpt { 41 | return func(h *Hook) error { 42 | h.getName = f 43 | return nil 44 | } 45 | } 46 | 47 | // WithEventOpts allows additional ETW event properties (keywords, tags, etc.) to be specified. 48 | func WithEventOpts(f func(*logrus.Entry) []etw.EventOpt) HookOpt { 49 | return func(h *Hook) error { 50 | h.getEventsOpts = f 51 | return nil 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /pkg/etw/wrapper_64.go: -------------------------------------------------------------------------------- 1 | //go:build windows && (amd64 || arm64) 2 | 3 | package etw 4 | 5 | import ( 6 | "github.com/Microsoft/go-winio/pkg/guid" 7 | "golang.org/x/sys/windows" 8 | ) 9 | 10 | func eventUnregister(providerHandle providerHandle) (win32err error) { 11 | return eventUnregister_64(providerHandle) 12 | } 13 | 14 | func eventWriteTransfer( 15 | providerHandle providerHandle, 16 | descriptor *eventDescriptor, 17 | activityID *windows.GUID, 18 | relatedActivityID *windows.GUID, 19 | dataDescriptorCount uint32, 20 | dataDescriptors *eventDataDescriptor) (win32err error) { 21 | return eventWriteTransfer_64( 22 | providerHandle, 23 | descriptor, 24 | activityID, 25 | relatedActivityID, 26 | dataDescriptorCount, 27 | dataDescriptors) 28 | } 29 | 30 | func eventSetInformation( 31 | providerHandle providerHandle, 32 | class eventInfoClass, 33 | information uintptr, 34 | length uint32) (win32err error) { 35 | return eventSetInformation_64( 36 | providerHandle, 37 | class, 38 | information, 39 | length) 40 | } 41 | 42 | // providerCallbackAdapter acts as the first-level callback from the C/ETW side 43 | // for provider notifications. Because Go has trouble with callback arguments of 44 | // different size, it has only pointer-sized arguments, which are then cast to 45 | // the appropriate types when calling providerCallback. 46 | func providerCallbackAdapter( 47 | sourceID *guid.GUID, 48 | state uintptr, 49 | level uintptr, 50 | matchAnyKeyword uintptr, 51 | matchAllKeyword uintptr, 52 | filterData uintptr, 53 | i uintptr, 54 | ) uintptr { 55 | providerCallback(*sourceID, 56 | ProviderState(state), 57 | Level(level), 58 | uint64(matchAnyKeyword), 59 | uint64(matchAllKeyword), 60 | filterData, 61 | i) 62 | return 0 63 | } 64 | -------------------------------------------------------------------------------- /vhd/vhd_test.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package vhd 4 | 5 | import ( 6 | "os" 7 | "path/filepath" 8 | "testing" 9 | 10 | "github.com/Microsoft/go-winio/pkg/guid" 11 | ) 12 | 13 | func TestVirtualDiskIdentifier(t *testing.T) { 14 | // Create a temporary directory for the test 15 | tempDir := t.TempDir() 16 | // TODO(ambarve): We should add a test for VHD too, but our current create VHD API 17 | // seem to only work for VHDX. 18 | vhdPath := filepath.Join(tempDir, "test.vhdx") 19 | 20 | // Create the virtual disk 21 | if err := CreateVhdx(vhdPath, 1, 1); err != nil { // 1GB, 1MB block size 22 | t.Fatalf("failed to create virtual disk: %s", err) 23 | } 24 | defer os.Remove(vhdPath) 25 | 26 | // Get the initial identifier 27 | initialID, err := GetVirtualDiskIdentifier(vhdPath) 28 | if err != nil { 29 | t.Fatalf("failed to get initial virtual disk identifier: %s", err) 30 | } 31 | t.Logf("initial identifier: %s", initialID.String()) 32 | 33 | // Create a new GUID to set 34 | newID := guid.GUID{ 35 | Data1: 0x12345678, 36 | Data2: 0x1234, 37 | Data3: 0x5678, 38 | Data4: [8]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0}, 39 | } 40 | t.Logf("setting new identifier: %s", newID.String()) 41 | 42 | // Set the new identifier 43 | if err := SetVirtualDiskIdentifier(vhdPath, newID); err != nil { 44 | t.Fatalf("failed to set virtual disk identifier: %s", err) 45 | } 46 | 47 | // Get the identifier again to verify it was set correctly 48 | retrievedID, err := GetVirtualDiskIdentifier(vhdPath) 49 | if err != nil { 50 | t.Fatalf("failed to get virtual disk identifier after setting: %s", err) 51 | } 52 | t.Logf("retrieved identifier: %s", retrievedID.String()) 53 | 54 | // Verify the retrieved ID matches the one we set 55 | if retrievedID != newID { 56 | t.Errorf("retrieved identifier does not match set identifier.\nExpected: %s\nGot: %s", 57 | newID.String(), retrievedID.String()) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /internal/socket/zsyscall_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | // Code generated by 'go generate' using "github.com/Microsoft/go-winio/tools/mkwinsyscall"; DO NOT EDIT. 4 | 5 | package socket 6 | 7 | import ( 8 | "syscall" 9 | "unsafe" 10 | 11 | "golang.org/x/sys/windows" 12 | ) 13 | 14 | var _ unsafe.Pointer 15 | 16 | // Do the interface allocations only once for common 17 | // Errno values. 18 | const ( 19 | errnoERROR_IO_PENDING = 997 20 | ) 21 | 22 | var ( 23 | errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING) 24 | errERROR_EINVAL error = syscall.EINVAL 25 | ) 26 | 27 | // errnoErr returns common boxed Errno values, to prevent 28 | // allocations at runtime. 29 | func errnoErr(e syscall.Errno) error { 30 | switch e { 31 | case 0: 32 | return errERROR_EINVAL 33 | case errnoERROR_IO_PENDING: 34 | return errERROR_IO_PENDING 35 | } 36 | return e 37 | } 38 | 39 | var ( 40 | modws2_32 = windows.NewLazySystemDLL("ws2_32.dll") 41 | 42 | procbind = modws2_32.NewProc("bind") 43 | procgetpeername = modws2_32.NewProc("getpeername") 44 | procgetsockname = modws2_32.NewProc("getsockname") 45 | ) 46 | 47 | func bind(s windows.Handle, name unsafe.Pointer, namelen int32) (err error) { 48 | r1, _, e1 := syscall.SyscallN(procbind.Addr(), uintptr(s), uintptr(name), uintptr(namelen)) 49 | if r1 == socketError { 50 | err = errnoErr(e1) 51 | } 52 | return 53 | } 54 | 55 | func getpeername(s windows.Handle, name unsafe.Pointer, namelen *int32) (err error) { 56 | r1, _, e1 := syscall.SyscallN(procgetpeername.Addr(), uintptr(s), uintptr(name), uintptr(unsafe.Pointer(namelen))) 57 | if r1 == socketError { 58 | err = errnoErr(e1) 59 | } 60 | return 61 | } 62 | 63 | func getsockname(s windows.Handle, name unsafe.Pointer, namelen *int32) (err error) { 64 | r1, _, e1 := syscall.SyscallN(procgetsockname.Addr(), uintptr(s), uintptr(name), uintptr(unsafe.Pointer(namelen))) 65 | if r1 == socketError { 66 | err = errnoErr(e1) 67 | } 68 | return 69 | } 70 | -------------------------------------------------------------------------------- /internal/computestorage/computestorage.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package computestorage 4 | 5 | import ( 6 | "fmt" 7 | 8 | "golang.org/x/sys/windows" 9 | 10 | "github.com/Microsoft/go-winio/internal/interop" 11 | ) 12 | 13 | //go:generate go run github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go computestorage.go 14 | 15 | // https://learn.microsoft.com/en-us/virtualization/api/hcs/reference/hcsformatwritablelayervhd 16 | // 17 | //sys hcsFormatWritableLayerVhd(handle windows.Handle) (hr error) = computestorage.HcsFormatWritableLayerVhd? 18 | 19 | // FormatWritableLayerVHD formats a virtual disk for use as a writable container layer. 20 | // 21 | // If the VHD is not mounted it will be temporarily mounted. 22 | // 23 | // NOTE: This API had a breaking change in the operating system after Windows Server 2019. 24 | // On ws2019 the API expects to get passed a file handle from CreateFile for the vhd that 25 | // the caller wants to format. On > ws2019, its expected that the caller passes a vhd handle 26 | // that can be obtained from the virtdisk APIs. 27 | func FormatWritableLayerVHD(vhdHandle windows.Handle) (err error) { 28 | err = hcsFormatWritableLayerVhd(vhdHandle) 29 | if err != nil { 30 | return fmt.Errorf("failed to format writable layer vhd: %w", err) 31 | } 32 | return nil 33 | } 34 | 35 | // https://learn.microsoft.com/en-us/virtualization/api/hcs/reference/hcsgetlayervhdmountpath 36 | // 37 | //sys hcsGetLayerVhdMountPath(vhdHandle windows.Handle, mountPath **uint16) (hr error) = computestorage.HcsGetLayerVhdMountPath? 38 | 39 | // GetLayerVHDMountPath returns the volume path for a virtual disk of a writable container layer. 40 | func GetLayerVHDMountPath(vhdHandle windows.Handle) (path string, err error) { 41 | var mountPath *uint16 42 | err = hcsGetLayerVhdMountPath(vhdHandle, &mountPath) 43 | if err != nil { 44 | return "", fmt.Errorf("failed to get vhd mount path: %w", err) 45 | } 46 | path = interop.ConvertAndFreeCoTaskMemString(mountPath) 47 | return path, nil 48 | } 49 | -------------------------------------------------------------------------------- /internal/fs/zsyscall_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | // Code generated by 'go generate' using "github.com/Microsoft/go-winio/tools/mkwinsyscall"; DO NOT EDIT. 4 | 5 | package fs 6 | 7 | import ( 8 | "syscall" 9 | "unsafe" 10 | 11 | "golang.org/x/sys/windows" 12 | ) 13 | 14 | var _ unsafe.Pointer 15 | 16 | // Do the interface allocations only once for common 17 | // Errno values. 18 | const ( 19 | errnoERROR_IO_PENDING = 997 20 | ) 21 | 22 | var ( 23 | errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING) 24 | errERROR_EINVAL error = syscall.EINVAL 25 | ) 26 | 27 | // errnoErr returns common boxed Errno values, to prevent 28 | // allocations at runtime. 29 | func errnoErr(e syscall.Errno) error { 30 | switch e { 31 | case 0: 32 | return errERROR_EINVAL 33 | case errnoERROR_IO_PENDING: 34 | return errERROR_IO_PENDING 35 | } 36 | return e 37 | } 38 | 39 | var ( 40 | modkernel32 = windows.NewLazySystemDLL("kernel32.dll") 41 | 42 | procCreateFileW = modkernel32.NewProc("CreateFileW") 43 | ) 44 | 45 | func CreateFile(name string, access AccessMask, mode FileShareMode, sa *windows.SecurityAttributes, createmode FileCreationDisposition, attrs FileFlagOrAttribute, templatefile windows.Handle) (handle windows.Handle, err error) { 46 | var _p0 *uint16 47 | _p0, err = syscall.UTF16PtrFromString(name) 48 | if err != nil { 49 | return 50 | } 51 | return _CreateFile(_p0, access, mode, sa, createmode, attrs, templatefile) 52 | } 53 | 54 | func _CreateFile(name *uint16, access AccessMask, mode FileShareMode, sa *windows.SecurityAttributes, createmode FileCreationDisposition, attrs FileFlagOrAttribute, templatefile windows.Handle) (handle windows.Handle, err error) { 55 | r0, _, e1 := syscall.SyscallN(procCreateFileW.Addr(), uintptr(unsafe.Pointer(name)), uintptr(access), uintptr(mode), uintptr(unsafe.Pointer(sa)), uintptr(createmode), uintptr(attrs), uintptr(templatefile)) 56 | handle = windows.Handle(r0) 57 | if handle == windows.InvalidHandle { 58 | err = errnoErr(e1) 59 | } 60 | return 61 | } 62 | -------------------------------------------------------------------------------- /internal/computestorage/zsyscall_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | // Code generated by 'go generate' using "github.com/Microsoft/go-winio/tools/mkwinsyscall"; DO NOT EDIT. 4 | 5 | package computestorage 6 | 7 | import ( 8 | "syscall" 9 | "unsafe" 10 | 11 | "golang.org/x/sys/windows" 12 | ) 13 | 14 | var _ unsafe.Pointer 15 | 16 | // Do the interface allocations only once for common 17 | // Errno values. 18 | const ( 19 | errnoERROR_IO_PENDING = 997 20 | ) 21 | 22 | var ( 23 | errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING) 24 | errERROR_EINVAL error = syscall.EINVAL 25 | ) 26 | 27 | // errnoErr returns common boxed Errno values, to prevent 28 | // allocations at runtime. 29 | func errnoErr(e syscall.Errno) error { 30 | switch e { 31 | case 0: 32 | return errERROR_EINVAL 33 | case errnoERROR_IO_PENDING: 34 | return errERROR_IO_PENDING 35 | } 36 | return e 37 | } 38 | 39 | var ( 40 | modcomputestorage = windows.NewLazySystemDLL("computestorage.dll") 41 | 42 | procHcsFormatWritableLayerVhd = modcomputestorage.NewProc("HcsFormatWritableLayerVhd") 43 | procHcsGetLayerVhdMountPath = modcomputestorage.NewProc("HcsGetLayerVhdMountPath") 44 | ) 45 | 46 | func hcsFormatWritableLayerVhd(handle windows.Handle) (hr error) { 47 | hr = procHcsFormatWritableLayerVhd.Find() 48 | if hr != nil { 49 | return 50 | } 51 | r0, _, _ := syscall.SyscallN(procHcsFormatWritableLayerVhd.Addr(), uintptr(handle)) 52 | if int32(r0) < 0 { 53 | if r0&0x1fff0000 == 0x00070000 { 54 | r0 &= 0xffff 55 | } 56 | hr = syscall.Errno(r0) 57 | } 58 | return 59 | } 60 | 61 | func hcsGetLayerVhdMountPath(vhdHandle windows.Handle, mountPath **uint16) (hr error) { 62 | hr = procHcsGetLayerVhdMountPath.Find() 63 | if hr != nil { 64 | return 65 | } 66 | r0, _, _ := syscall.SyscallN(procHcsGetLayerVhdMountPath.Addr(), uintptr(vhdHandle), uintptr(unsafe.Pointer(mountPath))) 67 | if int32(r0) < 0 { 68 | if r0&0x1fff0000 == 0x00070000 { 69 | r0 &= 0xffff 70 | } 71 | hr = syscall.Errno(r0) 72 | } 73 | return 74 | } 75 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 5 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 6 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 7 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 8 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 9 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 10 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 11 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 12 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 13 | golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= 14 | golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= 15 | golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= 16 | golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 17 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 18 | golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= 19 | golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 20 | golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= 21 | golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= 22 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 23 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 24 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 25 | -------------------------------------------------------------------------------- /pkg/process/zsyscall_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | // Code generated by 'go generate' using "github.com/Microsoft/go-winio/tools/mkwinsyscall"; DO NOT EDIT. 4 | 5 | package process 6 | 7 | import ( 8 | "syscall" 9 | "unsafe" 10 | 11 | "golang.org/x/sys/windows" 12 | ) 13 | 14 | var _ unsafe.Pointer 15 | 16 | // Do the interface allocations only once for common 17 | // Errno values. 18 | const ( 19 | errnoERROR_IO_PENDING = 997 20 | ) 21 | 22 | var ( 23 | errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING) 24 | errERROR_EINVAL error = syscall.EINVAL 25 | ) 26 | 27 | // errnoErr returns common boxed Errno values, to prevent 28 | // allocations at runtime. 29 | func errnoErr(e syscall.Errno) error { 30 | switch e { 31 | case 0: 32 | return errERROR_EINVAL 33 | case errnoERROR_IO_PENDING: 34 | return errERROR_IO_PENDING 35 | } 36 | return e 37 | } 38 | 39 | var ( 40 | modkernel32 = windows.NewLazySystemDLL("kernel32.dll") 41 | 42 | procK32EnumProcesses = modkernel32.NewProc("K32EnumProcesses") 43 | procK32GetProcessMemoryInfo = modkernel32.NewProc("K32GetProcessMemoryInfo") 44 | procQueryFullProcessImageNameW = modkernel32.NewProc("QueryFullProcessImageNameW") 45 | ) 46 | 47 | func enumProcesses(pids *uint32, bufferSize uint32, retBufferSize *uint32) (err error) { 48 | r1, _, e1 := syscall.SyscallN(procK32EnumProcesses.Addr(), uintptr(unsafe.Pointer(pids)), uintptr(bufferSize), uintptr(unsafe.Pointer(retBufferSize))) 49 | if r1 == 0 { 50 | err = errnoErr(e1) 51 | } 52 | return 53 | } 54 | 55 | func getProcessMemoryInfo(process handle, memCounters *ProcessMemoryCountersEx, size uint32) (err error) { 56 | r1, _, e1 := syscall.SyscallN(procK32GetProcessMemoryInfo.Addr(), uintptr(process), uintptr(unsafe.Pointer(memCounters)), uintptr(size)) 57 | if r1 == 0 { 58 | err = errnoErr(e1) 59 | } 60 | return 61 | } 62 | 63 | func queryFullProcessImageName(process handle, flags uint32, buffer *uint16, bufferSize *uint32) (err error) { 64 | r1, _, e1 := syscall.SyscallN(procQueryFullProcessImageNameW.Addr(), uintptr(process), uintptr(flags), uintptr(unsafe.Pointer(buffer)), uintptr(unsafe.Pointer(bufferSize))) 65 | if r1 == 0 { 66 | err = errnoErr(e1) 67 | } 68 | return 69 | } 70 | -------------------------------------------------------------------------------- /tools/mkwinsyscall/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | mkwinsyscall generates windows system call bodies 3 | 4 | It parses all files specified on command line containing function 5 | prototypes (like syscall_windows.go) and prints system call bodies 6 | to standard output. 7 | 8 | The prototypes are marked by lines beginning with "//sys" and read 9 | like func declarations if //sys is replaced by func, but: 10 | 11 | - The parameter lists must give a name for each argument. This 12 | includes return parameters. 13 | 14 | - The parameter lists must give a type for each argument: 15 | the (x, y, z int) shorthand is not allowed. 16 | 17 | - If the return parameter is an error number, it must be named err. 18 | 19 | - If go func name needs to be different from its winapi dll name, 20 | the winapi name could be specified at the end, after "=" sign, like 21 | 22 | //sys LoadLibrary(libname string) (handle uint32, err error) = LoadLibraryA 23 | 24 | - Each function that returns err needs to supply a condition, that 25 | return value of winapi will be tested against to detect failure. 26 | This would set err to windows "last-error", otherwise it will be nil. 27 | The value can be provided at end of //sys declaration, like 28 | 29 | //sys LoadLibrary(libname string) (handle uint32, err error) [failretval==-1] = LoadLibraryA 30 | 31 | and is [failretval==0] by default. 32 | 33 | - If the function name ends in a "?", then the function not existing is non- 34 | fatal, and an error will be returned instead of panicking. 35 | 36 | Usage: 37 | 38 | mkwinsyscall [flags] [path ...] 39 | 40 | Flags 41 | 42 | -output string 43 | Output file name (standard output if omitted). 44 | -sort 45 | Sort DLL and function declarations (default true). 46 | Intended to help transition from older versions of mkwinsyscall by making diffs 47 | easier to read and understand. 48 | -systemdll 49 | Whether all DLLs should be loaded from the Windows system directory (default true). 50 | -trace 51 | Generate print statement after every syscall. 52 | -utf16 53 | Encode string arguments as UTF-16 for syscalls not ending in 'A' or 'W' (default true). 54 | -winio 55 | Import this package ("github.com/Microsoft/go-winio"). 56 | */ 57 | package main 58 | -------------------------------------------------------------------------------- /backuptar/strconv.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package backuptar 4 | 5 | import ( 6 | "archive/tar" 7 | "fmt" 8 | "strconv" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | // Functions copied from https://github.com/golang/go/blob/master/src/archive/tar/strconv.go 14 | // as we need to manage the LIBARCHIVE.creationtime PAXRecord manually. 15 | // Idea taken from containerd which did the same thing. 16 | 17 | // parsePAXTime takes a string of the form %d.%d as described in the PAX 18 | // specification. Note that this implementation allows for negative timestamps, 19 | // which is allowed for by the PAX specification, but not always portable. 20 | func parsePAXTime(s string) (time.Time, error) { 21 | const maxNanoSecondDigits = 9 22 | 23 | // Split string into seconds and sub-seconds parts. 24 | ss, sn := s, "" 25 | if pos := strings.IndexByte(s, '.'); pos >= 0 { 26 | ss, sn = s[:pos], s[pos+1:] 27 | } 28 | 29 | // Parse the seconds. 30 | secs, err := strconv.ParseInt(ss, 10, 64) 31 | if err != nil { 32 | return time.Time{}, tar.ErrHeader 33 | } 34 | if len(sn) == 0 { 35 | return time.Unix(secs, 0), nil // No sub-second values 36 | } 37 | 38 | // Parse the nanoseconds. 39 | if strings.Trim(sn, "0123456789") != "" { 40 | return time.Time{}, tar.ErrHeader 41 | } 42 | if len(sn) < maxNanoSecondDigits { 43 | sn += strings.Repeat("0", maxNanoSecondDigits-len(sn)) // Right pad 44 | } else { 45 | sn = sn[:maxNanoSecondDigits] // Right truncate 46 | } 47 | nsecs, _ := strconv.ParseInt(sn, 10, 64) // Must succeed 48 | if len(ss) > 0 && ss[0] == '-' { 49 | return time.Unix(secs, -1*nsecs), nil // Negative correction 50 | } 51 | return time.Unix(secs, nsecs), nil 52 | } 53 | 54 | // formatPAXTime converts ts into a time of the form %d.%d as described in the 55 | // PAX specification. This function is capable of negative timestamps. 56 | func formatPAXTime(ts time.Time) (s string) { 57 | secs, nsecs := ts.Unix(), ts.Nanosecond() 58 | if nsecs == 0 { 59 | return strconv.FormatInt(secs, 10) 60 | } 61 | 62 | // If seconds is negative, then perform correction. 63 | sign := "" 64 | if secs < 0 { 65 | sign = "-" // Remember sign 66 | secs = -(secs + 1) // Add a second to secs 67 | nsecs = -(nsecs - 1e9) // Take that second away from nsecs 68 | } 69 | return strings.TrimRight(fmt.Sprintf("%s%d.%09d", sign, secs, nsecs), "0") 70 | } 71 | -------------------------------------------------------------------------------- /pkg/etw/wrapper_32.go: -------------------------------------------------------------------------------- 1 | //go:build windows && (386 || arm) 2 | 3 | package etw 4 | 5 | import ( 6 | "github.com/Microsoft/go-winio/pkg/guid" 7 | "golang.org/x/sys/windows" 8 | ) 9 | 10 | func low(v providerHandle) uint32 { 11 | return uint32(v & 0xffffffff) 12 | } 13 | 14 | func high(v providerHandle) uint32 { 15 | return low(v >> 32) 16 | } 17 | 18 | func eventUnregister(providerHandle providerHandle) (win32err error) { 19 | return eventUnregister_32(low(providerHandle), high(providerHandle)) 20 | } 21 | 22 | func eventWriteTransfer( 23 | providerHandle providerHandle, 24 | descriptor *eventDescriptor, 25 | activityID *windows.GUID, 26 | relatedActivityID *windows.GUID, 27 | dataDescriptorCount uint32, 28 | dataDescriptors *eventDataDescriptor) (win32err error) { 29 | 30 | return eventWriteTransfer_32( 31 | low(providerHandle), 32 | high(providerHandle), 33 | descriptor, 34 | activityID, 35 | relatedActivityID, 36 | dataDescriptorCount, 37 | dataDescriptors) 38 | } 39 | 40 | func eventSetInformation( 41 | providerHandle providerHandle, 42 | class eventInfoClass, 43 | information uintptr, 44 | length uint32) (win32err error) { 45 | 46 | return eventSetInformation_32( 47 | low(providerHandle), 48 | high(providerHandle), 49 | class, 50 | information, 51 | length) 52 | } 53 | 54 | // providerCallbackAdapter acts as the first-level callback from the C/ETW side 55 | // for provider notifications. Because Go has trouble with callback arguments of 56 | // different size, it has only pointer-sized arguments, which are then cast to 57 | // the appropriate types when calling providerCallback. 58 | // For x86, the matchAny and matchAll keywords need to be assembled from two 59 | // 32-bit integers, because the max size of an argument is uintptr, but those 60 | // two arguments are actually 64-bit integers. 61 | func providerCallbackAdapter(sourceID *guid.GUID, state uint32, level uint32, matchAnyKeyword_low uint32, matchAnyKeyword_high uint32, matchAllKeyword_low uint32, matchAllKeyword_high uint32, filterData uintptr, i uintptr) uintptr { 62 | matchAnyKeyword := uint64(matchAnyKeyword_high)<<32 | uint64(matchAnyKeyword_low) 63 | matchAllKeyword := uint64(matchAllKeyword_high)<<32 | uint64(matchAllKeyword_low) 64 | providerCallback(*sourceID, ProviderState(state), Level(level), uint64(matchAnyKeyword), uint64(matchAllKeyword), filterData, i) 65 | return 0 66 | } 67 | -------------------------------------------------------------------------------- /pkg/etw/eventopt.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package etw 4 | 5 | import ( 6 | "github.com/Microsoft/go-winio/pkg/guid" 7 | ) 8 | 9 | type eventOptions struct { 10 | descriptor *eventDescriptor 11 | activityID guid.GUID 12 | relatedActivityID guid.GUID 13 | tags uint32 14 | } 15 | 16 | // EventOpt defines the option function type that can be passed to 17 | // Provider.WriteEvent to specify general event options, such as level and 18 | // keyword. 19 | type EventOpt func(options *eventOptions) 20 | 21 | // WithEventOpts returns the variadic arguments as a single slice. 22 | func WithEventOpts(opts ...EventOpt) []EventOpt { 23 | return opts 24 | } 25 | 26 | // WithLevel specifies the level of the event to be written. 27 | func WithLevel(level Level) EventOpt { 28 | return func(options *eventOptions) { 29 | options.descriptor.level = level 30 | } 31 | } 32 | 33 | // WithKeyword specifies the keywords of the event to be written. Multiple uses 34 | // of this option are OR'd together. 35 | func WithKeyword(keyword uint64) EventOpt { 36 | return func(options *eventOptions) { 37 | options.descriptor.keyword |= keyword 38 | } 39 | } 40 | 41 | // WithChannel specifies the channel of the event to be written. 42 | func WithChannel(channel Channel) EventOpt { 43 | return func(options *eventOptions) { 44 | options.descriptor.channel = channel 45 | } 46 | } 47 | 48 | // WithOpcode specifies the opcode of the event to be written. 49 | func WithOpcode(opcode Opcode) EventOpt { 50 | return func(options *eventOptions) { 51 | options.descriptor.opcode = opcode 52 | } 53 | } 54 | 55 | // WithTags specifies the tags of the event to be written. Tags is a 28-bit 56 | // value (top 4 bits are ignored) which are interpreted by the event consumer. 57 | func WithTags(newTags uint32) EventOpt { 58 | return func(options *eventOptions) { 59 | options.tags |= newTags 60 | } 61 | } 62 | 63 | // WithActivityID specifies the activity ID of the event to be written. 64 | func WithActivityID(activityID guid.GUID) EventOpt { 65 | return func(options *eventOptions) { 66 | options.activityID = activityID 67 | } 68 | } 69 | 70 | // WithRelatedActivityID specifies the parent activity ID of the event to be written. 71 | func WithRelatedActivityID(activityID guid.GUID) EventOpt { 72 | return func(options *eventOptions) { 73 | options.relatedActivityID = activityID 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /pkg/etw/eventdata.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package etw 4 | 5 | import ( 6 | "bytes" 7 | "encoding/binary" 8 | 9 | "golang.org/x/sys/windows" 10 | ) 11 | 12 | // eventData maintains a buffer which builds up the data for an ETW event. It 13 | // needs to be paired with EventMetadata which describes the event. 14 | type eventData struct { 15 | buffer bytes.Buffer 16 | } 17 | 18 | // toBytes returns the raw binary data containing the event data. The returned 19 | // value is not copied from the internal buffer, so it can be mutated by the 20 | // eventData object after it is returned. 21 | func (ed *eventData) toBytes() []byte { 22 | return ed.buffer.Bytes() 23 | } 24 | 25 | // writeString appends a string, including the null terminator, to the buffer. 26 | func (ed *eventData) writeString(data string) { 27 | _, _ = ed.buffer.WriteString(data) 28 | _ = ed.buffer.WriteByte(0) 29 | } 30 | 31 | // writeInt8 appends a int8 to the buffer. 32 | func (ed *eventData) writeInt8(value int8) { 33 | _ = ed.buffer.WriteByte(uint8(value)) 34 | } 35 | 36 | // writeInt16 appends a int16 to the buffer. 37 | func (ed *eventData) writeInt16(value int16) { 38 | _ = binary.Write(&ed.buffer, binary.LittleEndian, value) 39 | } 40 | 41 | // writeInt32 appends a int32 to the buffer. 42 | func (ed *eventData) writeInt32(value int32) { 43 | _ = binary.Write(&ed.buffer, binary.LittleEndian, value) 44 | } 45 | 46 | // writeInt64 appends a int64 to the buffer. 47 | func (ed *eventData) writeInt64(value int64) { 48 | _ = binary.Write(&ed.buffer, binary.LittleEndian, value) 49 | } 50 | 51 | // writeUint8 appends a uint8 to the buffer. 52 | func (ed *eventData) writeUint8(value uint8) { 53 | _ = ed.buffer.WriteByte(value) 54 | } 55 | 56 | // writeUint16 appends a uint16 to the buffer. 57 | func (ed *eventData) writeUint16(value uint16) { 58 | _ = binary.Write(&ed.buffer, binary.LittleEndian, value) 59 | } 60 | 61 | // writeUint32 appends a uint32 to the buffer. 62 | func (ed *eventData) writeUint32(value uint32) { 63 | _ = binary.Write(&ed.buffer, binary.LittleEndian, value) 64 | } 65 | 66 | // writeUint64 appends a uint64 to the buffer. 67 | func (ed *eventData) writeUint64(value uint64) { 68 | _ = binary.Write(&ed.buffer, binary.LittleEndian, value) 69 | } 70 | 71 | // writeFiletime appends a FILETIME to the buffer. 72 | func (ed *eventData) writeFiletime(value windows.Filetime) { 73 | _ = binary.Write(&ed.buffer, binary.LittleEndian, value) 74 | } 75 | -------------------------------------------------------------------------------- /pkg/security/zsyscall_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | // Code generated by 'go generate' using "github.com/Microsoft/go-winio/tools/mkwinsyscall"; DO NOT EDIT. 4 | 5 | package security 6 | 7 | import ( 8 | "syscall" 9 | "unsafe" 10 | 11 | "golang.org/x/sys/windows" 12 | ) 13 | 14 | var _ unsafe.Pointer 15 | 16 | // Do the interface allocations only once for common 17 | // Errno values. 18 | const ( 19 | errnoERROR_IO_PENDING = 997 20 | ) 21 | 22 | var ( 23 | errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING) 24 | errERROR_EINVAL error = syscall.EINVAL 25 | ) 26 | 27 | // errnoErr returns common boxed Errno values, to prevent 28 | // allocations at runtime. 29 | func errnoErr(e syscall.Errno) error { 30 | switch e { 31 | case 0: 32 | return errERROR_EINVAL 33 | case errnoERROR_IO_PENDING: 34 | return errERROR_IO_PENDING 35 | } 36 | return e 37 | } 38 | 39 | var ( 40 | modadvapi32 = windows.NewLazySystemDLL("advapi32.dll") 41 | 42 | procGetSecurityInfo = modadvapi32.NewProc("GetSecurityInfo") 43 | procSetEntriesInAclW = modadvapi32.NewProc("SetEntriesInAclW") 44 | procSetSecurityInfo = modadvapi32.NewProc("SetSecurityInfo") 45 | ) 46 | 47 | func getSecurityInfo(handle windows.Handle, objectType uint32, si uint32, ppsidOwner **uintptr, ppsidGroup **uintptr, ppDacl *uintptr, ppSacl *uintptr, ppSecurityDescriptor *uintptr) (win32err error) { 48 | r0, _, _ := syscall.SyscallN(procGetSecurityInfo.Addr(), uintptr(handle), uintptr(objectType), uintptr(si), uintptr(unsafe.Pointer(ppsidOwner)), uintptr(unsafe.Pointer(ppsidGroup)), uintptr(unsafe.Pointer(ppDacl)), uintptr(unsafe.Pointer(ppSacl)), uintptr(unsafe.Pointer(ppSecurityDescriptor))) 49 | if r0 != 0 { 50 | win32err = syscall.Errno(r0) 51 | } 52 | return 53 | } 54 | 55 | func setEntriesInAcl(count uintptr, pListOfEEs uintptr, oldAcl uintptr, newAcl *uintptr) (win32err error) { 56 | r0, _, _ := syscall.SyscallN(procSetEntriesInAclW.Addr(), uintptr(count), uintptr(pListOfEEs), uintptr(oldAcl), uintptr(unsafe.Pointer(newAcl))) 57 | if r0 != 0 { 58 | win32err = syscall.Errno(r0) 59 | } 60 | return 61 | } 62 | 63 | func setSecurityInfo(handle windows.Handle, objectType uint32, si uint32, psidOwner uintptr, psidGroup uintptr, pDacl uintptr, pSacl uintptr) (win32err error) { 64 | r0, _, _ := syscall.SyscallN(procSetSecurityInfo.Addr(), uintptr(handle), uintptr(objectType), uintptr(si), uintptr(psidOwner), uintptr(psidGroup), uintptr(pDacl), uintptr(pSacl)) 65 | if r0 != 0 { 66 | win32err = syscall.Errno(r0) 67 | } 68 | return 69 | } 70 | -------------------------------------------------------------------------------- /ea_test.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package winio 4 | 5 | import ( 6 | "os" 7 | "reflect" 8 | "testing" 9 | "unsafe" 10 | 11 | "golang.org/x/sys/windows" 12 | ) 13 | 14 | var ( 15 | testEas = []ExtendedAttribute{ 16 | {Name: "foo", Value: []byte("bar")}, 17 | {Name: "fizz", Value: []byte("buzz")}, 18 | } 19 | 20 | testEasEncoded = []byte{16, 0, 0, 0, 0, 3, 3, 0, 102, 111, 111, 0, 98, 97, 114, 0, 0, 21 | 0, 0, 0, 0, 4, 4, 0, 102, 105, 122, 122, 0, 98, 117, 122, 122, 0, 0, 0} 22 | testEasNotPadded = testEasEncoded[0 : len(testEasEncoded)-3] 23 | testEasTruncated = testEasEncoded[0:20] 24 | ) 25 | 26 | func Test_RoundTripEas(t *testing.T) { 27 | b, err := EncodeExtendedAttributes(testEas) 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | if !reflect.DeepEqual(testEasEncoded, b) { 32 | t.Fatalf("encoded mismatch %v %v", testEasEncoded, b) 33 | } 34 | eas, err := DecodeExtendedAttributes(b) 35 | if err != nil { 36 | t.Fatal(err) 37 | } 38 | if !reflect.DeepEqual(testEas, eas) { 39 | t.Fatalf("mismatch %+v %+v", testEas, eas) 40 | } 41 | } 42 | 43 | func Test_EasDontNeedPaddingAtEnd(t *testing.T) { 44 | eas, err := DecodeExtendedAttributes(testEasNotPadded) 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | if !reflect.DeepEqual(testEas, eas) { 49 | t.Fatalf("mismatch %+v %+v", testEas, eas) 50 | } 51 | } 52 | 53 | func Test_TruncatedEasFailCorrectly(t *testing.T) { 54 | _, err := DecodeExtendedAttributes(testEasTruncated) 55 | if err == nil { 56 | t.Fatal("expected error") 57 | } 58 | } 59 | 60 | func Test_NilEasEncodeAndDecodeAsNil(t *testing.T) { 61 | b, err := EncodeExtendedAttributes(nil) 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | if len(b) != 0 { 66 | t.Fatal("expected empty") 67 | } 68 | eas, err := DecodeExtendedAttributes(nil) 69 | if err != nil { 70 | t.Fatal(err) 71 | } 72 | if len(eas) != 0 { 73 | t.Fatal("expected empty") 74 | } 75 | } 76 | 77 | // Test_SetFileEa makes sure that the test buffer is actually parsable by NtSetEaFile. 78 | func Test_SetFileEa(t *testing.T) { 79 | f, err := os.CreateTemp("", "winio") 80 | if err != nil { 81 | t.Fatal(err) 82 | } 83 | defer os.Remove(f.Name()) 84 | defer f.Close() 85 | ntdll := windows.MustLoadDLL("ntdll.dll") 86 | ntSetEaFile := ntdll.MustFindProc("NtSetEaFile") 87 | var iosb [2]uintptr 88 | r, _, _ := ntSetEaFile.Call(f.Fd(), 89 | uintptr(unsafe.Pointer(&iosb[0])), 90 | uintptr(unsafe.Pointer(&testEasEncoded[0])), 91 | uintptr(len(testEasEncoded))) 92 | if r != 0 { 93 | t.Fatalf("NtSetEaFile failed with %08x", r) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /pkg/etw/newprovider.go: -------------------------------------------------------------------------------- 1 | //go:build windows && (amd64 || arm64 || 386) 2 | 3 | package etw 4 | 5 | import ( 6 | "bytes" 7 | "encoding/binary" 8 | "unsafe" 9 | 10 | "github.com/Microsoft/go-winio/pkg/guid" 11 | "golang.org/x/sys/windows" 12 | ) 13 | 14 | // NewProviderWithOptions creates and registers a new ETW provider, allowing 15 | // the provider ID and Group to be manually specified. This is most useful when 16 | // there is an existing provider ID that must be used to conform to existing 17 | // diagnostic infrastructure. 18 | func NewProviderWithOptions(name string, options ...ProviderOpt) (provider *Provider, err error) { 19 | var opts providerOpts 20 | for _, opt := range options { 21 | opt(&opts) 22 | } 23 | 24 | if opts.id == (guid.GUID{}) { 25 | opts.id = providerIDFromName(name) 26 | } 27 | 28 | providerCallbackOnce.Do(func() { 29 | globalProviderCallback = windows.NewCallback(providerCallbackAdapter) 30 | }) 31 | 32 | provider = providers.newProvider() 33 | defer func(provider *Provider) { 34 | if err != nil { 35 | providers.removeProvider(provider) 36 | } 37 | }(provider) 38 | provider.ID = opts.id 39 | provider.callback = opts.callback 40 | 41 | if err := eventRegister((*windows.GUID)(&provider.ID), globalProviderCallback, uintptr(provider.index), &provider.handle); err != nil { 42 | return nil, err 43 | } 44 | 45 | trait := &bytes.Buffer{} 46 | if opts.group != (guid.GUID{}) { 47 | _ = binary.Write(trait, binary.LittleEndian, uint16(0)) // Write empty size for buffer (update later) 48 | _ = binary.Write(trait, binary.LittleEndian, uint8(1)) // EtwProviderTraitTypeGroup 49 | traitArray := opts.group.ToWindowsArray() // Append group guid 50 | trait.Write(traitArray[:]) 51 | binary.LittleEndian.PutUint16(trait.Bytes(), uint16(trait.Len())) // Update size 52 | } 53 | 54 | metadata := &bytes.Buffer{} 55 | _ = binary.Write(metadata, binary.LittleEndian, uint16(0)) // Write empty size for buffer (to update later) 56 | metadata.WriteString(name) 57 | metadata.WriteByte(0) // Null terminator for name 58 | _, _ = trait.WriteTo(metadata) // Add traits if applicable 59 | binary.LittleEndian.PutUint16(metadata.Bytes(), uint16(metadata.Len())) // Update the size at the beginning of the buffer 60 | provider.metadata = metadata.Bytes() 61 | 62 | if err := eventSetInformation( 63 | provider.handle, 64 | eventInfoClassProviderSetTraits, 65 | uintptr(unsafe.Pointer(&provider.metadata[0])), 66 | uint32(len(provider.metadata)), 67 | ); err != nil { 68 | return nil, err 69 | } 70 | 71 | return provider, nil 72 | } 73 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /pkg/etw/sample/main_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | // Shows a sample usage of the ETW logging package. 4 | package main 5 | 6 | import ( 7 | "bufio" 8 | "fmt" 9 | "log" 10 | "os" 11 | "runtime" 12 | 13 | "github.com/Microsoft/go-winio/pkg/etw" 14 | "github.com/Microsoft/go-winio/pkg/guid" 15 | ) 16 | 17 | func callback(sourceID guid.GUID, state etw.ProviderState, level etw.Level, matchAnyKeyword uint64, matchAllKeyword uint64, filterData uintptr) { 18 | fmt.Printf("Callback: isEnabled=%d, level=%d, matchAnyKeyword=%d\n", state, level, matchAnyKeyword) 19 | } 20 | 21 | func main() { 22 | fmt.Printf("Running on %s/%s\n", runtime.GOOS, runtime.GOARCH) 23 | 24 | group, err := guid.FromString("12341234-abcd-abcd-abcd-123412341234") 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | 29 | provider, err := etw.NewProvider("TestProvider", callback) 30 | 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | defer func() { 35 | if err := provider.Close(); err != nil { 36 | log.Fatal(err) 37 | } 38 | }() 39 | 40 | providerWithGroup, err := etw.NewProviderWithOptions("TestProviderWithGroup", etw.WithGroup(group), etw.WithCallback(callback)) 41 | 42 | if err != nil { 43 | log.Fatal(err) 44 | } 45 | defer func() { 46 | if err := providerWithGroup.Close(); err != nil { 47 | log.Fatal(err) 48 | } 49 | }() 50 | 51 | fmt.Printf("Provider ID: %s\n", provider) 52 | fmt.Printf("Provider w/ Group ID: %s\n", providerWithGroup) 53 | 54 | reader := bufio.NewReader(os.Stdin) 55 | 56 | fmt.Println("Press enter to log events") 57 | reader.ReadString('\n') 58 | 59 | if err := provider.WriteEvent( 60 | "TestEvent", 61 | etw.WithEventOpts( 62 | etw.WithLevel(etw.LevelInfo), 63 | etw.WithKeyword(0x140), 64 | ), 65 | etw.WithFields( 66 | etw.StringField("TestField", "Foo"), 67 | etw.StringField("TestField2", "Bar"), 68 | etw.Struct("TestStruct", 69 | etw.StringField("Field1", "Value1"), 70 | etw.StringField("Field2", "Value2")), 71 | etw.StringArray("TestArray", []string{ 72 | "Item1", 73 | "Item2", 74 | "Item3", 75 | "Item4", 76 | "Item5", 77 | })), 78 | ); err != nil { 79 | log.Fatal(err) 80 | } 81 | 82 | if err := providerWithGroup.WriteEvent( 83 | "TestEventWithGroup", 84 | etw.WithEventOpts( 85 | etw.WithLevel(etw.LevelInfo), 86 | etw.WithKeyword(0x140), 87 | ), 88 | etw.WithFields( 89 | etw.StringField("TestField", "Foo"), 90 | etw.StringField("TestField2", "Bar"), 91 | etw.Struct("TestStruct", 92 | etw.StringField("Field1", "Value1"), 93 | etw.StringField("Field2", "Value2")), 94 | etw.StringArray("TestArray", []string{ 95 | "Item1", 96 | "Item2", 97 | "Item3", 98 | "Item4", 99 | "Item5", 100 | })), 101 | ); err != nil { 102 | log.Fatal(err) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /hvsock_go118_test.go: -------------------------------------------------------------------------------- 1 | //go:build windows && go1.18 2 | 3 | package winio 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "testing" 9 | "time" 10 | 11 | "golang.org/x/sys/windows" 12 | ) 13 | 14 | func FuzzHvSockRxTx(f *testing.F) { 15 | // fuzzing fails on windows 2019 for some reason, even though tests pass 16 | if _, _, build := windows.RtlGetNtVersionNumbers(); build <= 17763 { 17 | f.Skipf("build (%d) must be > %d", build, 17763) 18 | } 19 | 20 | for _, b := range [][]byte{ 21 | []byte("hello?"), 22 | []byte("This is a really long string that should be a good example of the really long " + 23 | "payloads that may be sent over hvsockets when really long inputs are being used, tautologically. " + 24 | "That means that we will have to test with really long input sequences, which means that " + 25 | "we need to include really long byte sequences or strings in our testing so that we know that " + 26 | "the sockets can deal with really long inputs. Look at this key mashing: " + 27 | "sdflhsdfgkjdhskljjsad;kljfasd;lfkjsadl ;fasdjfopiwej09q34iur092\"i4o[piwajfliasdkf-012ior]-" + 28 | "01oi3;'lSD= len(r.chunks) { 95 | return io.EOF 96 | } 97 | if r.d != nil { 98 | r.d.Close() 99 | } 100 | r.curChunk = n 101 | size := r.chunkSize(n) 102 | uncompressedSize := r.uncompressedSize(n) 103 | section := io.NewSectionReader(r.r, r.chunkOffset(n), int64(size)) 104 | if size != uncompressedSize { 105 | d, err := lzx.NewReader(section, uncompressedSize) 106 | if err != nil { 107 | return err 108 | } 109 | r.d = d 110 | } else { 111 | r.d = io.NopCloser(section) 112 | } 113 | 114 | return nil 115 | } 116 | 117 | func (r *compressedReader) Read(b []byte) (int, error) { 118 | for { 119 | n, err := r.d.Read(b) 120 | if err != io.EOF { //nolint:errorlint 121 | return n, err 122 | } 123 | 124 | err = r.reset(r.curChunk + 1) 125 | if err != nil { 126 | return n, err 127 | } 128 | } 129 | } 130 | 131 | func (r *compressedReader) Close() error { 132 | var err error 133 | if r.d != nil { 134 | err = r.d.Close() 135 | r.d = nil 136 | } 137 | return err 138 | } 139 | -------------------------------------------------------------------------------- /pkg/etw/eventdescriptor.go: -------------------------------------------------------------------------------- 1 | package etw 2 | 3 | import "fmt" 4 | 5 | // Channel represents the ETW logging channel that is used. It can be used by 6 | // event consumers to give an event special treatment. 7 | type Channel uint8 8 | 9 | const ( 10 | // ChannelTraceLogging is the default channel for TraceLogging events. It is 11 | // not required to be used for TraceLogging, but will prevent decoding 12 | // issues for these events on older operating systems. 13 | ChannelTraceLogging Channel = 11 14 | ) 15 | 16 | // Level represents the ETW logging level. There are several predefined levels 17 | // that are commonly used, but technically anything from 0-255 is allowed. 18 | // Lower levels indicate more important events, and 0 indicates an event that 19 | // will always be collected. 20 | type Level uint8 21 | 22 | var _ fmt.Stringer = Level(0) 23 | 24 | // Predefined ETW log levels from winmeta.xml in the Windows SDK. 25 | // 26 | //go:generate go run golang.org/x/tools/cmd/stringer -type=Level -trimprefix=Level 27 | const ( 28 | LevelAlways Level = iota 29 | LevelCritical 30 | LevelError 31 | LevelWarning 32 | LevelInfo 33 | LevelVerbose 34 | ) 35 | 36 | // Opcode represents the operation that the event indicates is being performed. 37 | type Opcode uint8 38 | 39 | var _ fmt.Stringer = Opcode(0) 40 | 41 | // Predefined ETW opcodes from winmeta.xml in the Windows SDK. 42 | // 43 | //go:generate go run golang.org/x/tools/cmd/stringer -type=Opcode -trimprefix=Opcode 44 | const ( 45 | // OpcodeInfo indicates an informational event. 46 | OpcodeInfo Opcode = iota 47 | // OpcodeStart indicates the start of an operation. 48 | OpcodeStart 49 | // OpcodeStop indicates the end of an operation. 50 | OpcodeStop 51 | // OpcodeDCStart indicates the start of a provider capture state operation. 52 | OpcodeDCStart 53 | // OpcodeDCStop indicates the end of a provider capture state operation. 54 | OpcodeDCStop 55 | ) 56 | 57 | // eventDescriptor represents various metadata for an ETW event. 58 | type eventDescriptor struct { 59 | id uint16 60 | version uint8 61 | channel Channel 62 | level Level 63 | opcode Opcode 64 | task uint16 65 | keyword uint64 66 | } 67 | 68 | // newEventDescriptor returns an EventDescriptor initialized for use with 69 | // TraceLogging. 70 | func newEventDescriptor() *eventDescriptor { 71 | // Standard TraceLogging events default to the TraceLogging channel, and 72 | // verbose level. 73 | return &eventDescriptor{ 74 | channel: ChannelTraceLogging, 75 | level: LevelVerbose, 76 | } 77 | } 78 | 79 | // identity returns the identity of the event. If the identity is not 0, it 80 | // should uniquely identify the other event metadata (contained in 81 | // EventDescriptor, and field metadata). Only the lower 24 bits of this value 82 | // are relevant. 83 | // 84 | //nolint:unused // keep for future use 85 | func (ed *eventDescriptor) identity() uint32 { 86 | return (uint32(ed.version) << 16) | uint32(ed.id) 87 | } 88 | 89 | // setIdentity sets the identity of the event. If the identity is not 0, it 90 | // should uniquely identify the other event metadata (contained in 91 | // EventDescriptor, and field metadata). Only the lower 24 bits of this value 92 | // are relevant. 93 | // 94 | //nolint:unused // keep for future use 95 | func (ed *eventDescriptor) setIdentity(identity uint32) { 96 | ed.id = uint16(identity) 97 | ed.version = uint8(identity >> 16) 98 | } 99 | -------------------------------------------------------------------------------- /pkg/security/grantvmgroupaccess_test.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package security 4 | 5 | import ( 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | "regexp" 10 | "strings" 11 | "testing" 12 | ) 13 | 14 | const ( 15 | vmAccountName = `NT VIRTUAL MACHINE\\Virtual Machines` 16 | vmAccountSID = "S-1-5-83-0" 17 | ) 18 | 19 | // TestGrantVmGroupAccess verifies for the three case of a file, a directory, 20 | // and a file in a directory that the appropriate ACEs are set, including 21 | // inheritance in the second two examples. These are the expected ACES. Is 22 | // verified by running icacls and comparing output. 23 | // 24 | // File: 25 | // S-1-15-3-1024-2268835264-3721307629-241982045-173645152-1490879176-104643441-2915960892-1612460704:(R,W) 26 | // S-1-5-83-1-3166535780-1122986932-343720105-43916321:(R,W) 27 | // 28 | // Directory: 29 | // S-1-15-3-1024-2268835264-3721307629-241982045-173645152-1490879176-104643441-2915960892-1612460704:(OI)(CI)(R,W) 30 | // S-1-5-83-1-3166535780-1122986932-343720105-43916321:(OI)(CI)(R,W) 31 | // 32 | // File in directory (inherited): 33 | // S-1-15-3-1024-2268835264-3721307629-241982045-173645152-1490879176-104643441-2915960892-1612460704:(I)(R,W) 34 | // S-1-5-83-1-3166535780-1122986932-343720105-43916321:(I)(R,W) 35 | 36 | func TestGrantVmGroupAccess(t *testing.T) { 37 | f, err := os.CreateTemp("", "gvmgafile") 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | defer func() { 42 | f.Close() 43 | os.Remove(f.Name()) 44 | }() 45 | 46 | d := t.TempDir() 47 | find, err := os.Create(filepath.Join(d, "find.txt")) 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | defer find.Close() 52 | 53 | if err := GrantVmGroupAccess(f.Name()); err != nil { 54 | t.Fatal(err) 55 | } 56 | 57 | if err := GrantVmGroupAccess(d); err != nil { 58 | t.Fatal(err) 59 | } 60 | 61 | verifyVMAccountDACLs(t, 62 | f.Name(), 63 | []string{`(R)`}, 64 | ) 65 | 66 | // Two items here: 67 | // - One explicit read only. 68 | // - Other applies to this folder, subfolders and files 69 | // (OI): object inherit 70 | // (CI): container inherit 71 | // (IO): inherit only 72 | // (GR): generic read 73 | // 74 | // In properties for the directory, advanced security settings, this will 75 | // show as a single line "Allow/Virtual Machines/Read/Inherited from none/This folder, subfolder and files 76 | verifyVMAccountDACLs(t, 77 | d, 78 | []string{`(R)`, `(OI)(CI)(IO)(GR)`}, 79 | ) 80 | 81 | verifyVMAccountDACLs(t, 82 | find.Name(), 83 | []string{`(I)(R)`}, 84 | ) 85 | } 86 | 87 | func verifyVMAccountDACLs(t *testing.T, name string, permissions []string) { 88 | t.Helper() 89 | 90 | cmd := exec.Command("icacls", name) 91 | outb, err := cmd.CombinedOutput() 92 | if err != nil { 93 | t.Fatal(err) 94 | } 95 | out := string(outb) 96 | 97 | for _, p := range permissions { 98 | // Avoid '(' and ')' being part of match groups 99 | p = strings.Replace(p, "(", "\\(", -1) 100 | p = strings.Replace(p, ")", "\\)", -1) 101 | 102 | nameToCheck := vmAccountName + ":" + p 103 | sidToCheck := vmAccountSID + ":" + p 104 | 105 | rxName := regexp.MustCompile(nameToCheck) 106 | rxSID := regexp.MustCompile(sidToCheck) 107 | 108 | matchesName := rxName.FindAllStringIndex(out, -1) 109 | matchesSID := rxSID.FindAllStringIndex(out, -1) 110 | 111 | if len(matchesName) != 1 && len(matchesSID) != 1 { 112 | t.Fatalf("expected one match for %s or %s\n%s", nameToCheck, sidToCheck, out) 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /pkg/etwlogrus/hook_test.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package etwlogrus 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/sirupsen/logrus" 9 | ) 10 | 11 | func fireEvent(name string, value interface{}) { 12 | logrus.WithField("Field", value).Info(name) 13 | } 14 | 15 | // The purpose of this test is to log lots of different field types, to test the 16 | // logic that converts them to ETW. Because we don't have a way to 17 | // programatically validate the ETW events, this test has two main purposes: (1) 18 | // validate nothing causes a panic while logging (2) allow manual validation that 19 | // the data is logged correctly (through a tool like WPA). 20 | func TestFieldLogging(t *testing.T) { 21 | // Sample WPRP to collect this provider is included in HookTest.wprp. 22 | // 23 | // Start collection: 24 | // wpr -start HookTest.wprp -filemode 25 | // 26 | // Stop collection: 27 | // wpr -stop HookTest.etl 28 | h, err := NewHook("HookTest") 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | logrus.AddHook(h) 33 | 34 | fireEvent("Bool", true) 35 | fireEvent("BoolSlice", []bool{true, false, true}) 36 | fireEvent("EmptyBoolSlice", []bool{}) 37 | fireEvent("String", "teststring") 38 | fireEvent("StringSlice", []string{"sstr1", "sstr2", "sstr3"}) 39 | fireEvent("EmptyStringSlice", []string{}) 40 | fireEvent("Int", int(1)) 41 | fireEvent("IntSlice", []int{2, 3, 4}) 42 | fireEvent("EmptyIntSlice", []int{}) 43 | fireEvent("Int8", int8(5)) 44 | fireEvent("Int8Slice", []int8{6, 7, 8}) 45 | fireEvent("EmptyInt8Slice", []int8{}) 46 | fireEvent("Int16", int16(9)) 47 | fireEvent("Int16Slice", []int16{10, 11, 12}) 48 | fireEvent("EmptyInt16Slice", []int16{}) 49 | fireEvent("Int32", int32(13)) 50 | fireEvent("Int32Slice", []int32{14, 15, 16}) 51 | fireEvent("EmptyInt32Slice", []int32{}) 52 | fireEvent("Int64", int64(17)) 53 | fireEvent("Int64Slice", []int64{18, 19, 20}) 54 | fireEvent("EmptyInt64Slice", []int64{}) 55 | fireEvent("Uint", uint(21)) 56 | fireEvent("UintSlice", []uint{22, 23, 24}) 57 | fireEvent("EmptyUintSlice", []uint{}) 58 | fireEvent("Uint8", uint8(25)) 59 | fireEvent("Uint8Slice", []uint8{26, 27, 28}) 60 | fireEvent("EmptyUint8Slice", []uint8{}) 61 | fireEvent("Uint16", uint16(29)) 62 | fireEvent("Uint16Slice", []uint16{30, 31, 32}) 63 | fireEvent("EmptyUint16Slice", []uint16{}) 64 | fireEvent("Uint32", uint32(33)) 65 | fireEvent("Uint32Slice", []uint32{34, 35, 36}) 66 | fireEvent("EmptyUint32Slice", []uint32{}) 67 | fireEvent("Uint64", uint64(37)) 68 | fireEvent("Uint64Slice", []uint64{38, 39, 40}) 69 | fireEvent("EmptyUint64Slice", []uint64{}) 70 | fireEvent("Uintptr", uintptr(41)) 71 | fireEvent("UintptrSlice", []uintptr{42, 43, 44}) 72 | fireEvent("EmptyUintptrSlice", []uintptr{}) 73 | fireEvent("Float32", float32(45.46)) 74 | fireEvent("Float32Slice", []float32{47.48, 49.50, 51.52}) 75 | fireEvent("EmptyFloat32Slice", []float32{}) 76 | fireEvent("Float64", float64(53.54)) 77 | fireEvent("Float64Slice", []float64{55.56, 57.58, 59.60}) 78 | fireEvent("EmptyFloat64Slice", []float64{}) 79 | 80 | type struct1 struct { 81 | A float32 82 | priv int 83 | B []uint 84 | } 85 | type struct2 struct { 86 | A int 87 | B int 88 | } 89 | type struct3 struct { 90 | struct2 91 | A int 92 | B string 93 | priv string 94 | C struct1 95 | D uint16 96 | } 97 | // Unexported fields, and fields in embedded structs, should not log. 98 | fireEvent("Struct", struct3{struct2{-1, -2}, 1, "2s", "-3s", struct1{3.4, -4, []uint{5, 6, 7}}, 8}) 99 | } 100 | -------------------------------------------------------------------------------- /pkg/bindfilter/zsyscall_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | // Code generated by 'go generate' using "github.com/Microsoft/go-winio/tools/mkwinsyscall"; DO NOT EDIT. 4 | 5 | package bindfilter 6 | 7 | import ( 8 | "syscall" 9 | "unsafe" 10 | 11 | "golang.org/x/sys/windows" 12 | ) 13 | 14 | var _ unsafe.Pointer 15 | 16 | // Do the interface allocations only once for common 17 | // Errno values. 18 | const ( 19 | errnoERROR_IO_PENDING = 997 20 | ) 21 | 22 | var ( 23 | errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING) 24 | errERROR_EINVAL error = syscall.EINVAL 25 | ) 26 | 27 | // errnoErr returns common boxed Errno values, to prevent 28 | // allocations at runtime. 29 | func errnoErr(e syscall.Errno) error { 30 | switch e { 31 | case 0: 32 | return errERROR_EINVAL 33 | case errnoERROR_IO_PENDING: 34 | return errERROR_IO_PENDING 35 | } 36 | return e 37 | } 38 | 39 | var ( 40 | modbindfltapi = windows.NewLazySystemDLL("bindfltapi.dll") 41 | 42 | procBfGetMappings = modbindfltapi.NewProc("BfGetMappings") 43 | procBfRemoveMapping = modbindfltapi.NewProc("BfRemoveMapping") 44 | procBfSetupFilter = modbindfltapi.NewProc("BfSetupFilter") 45 | ) 46 | 47 | func bfGetMappings(flags uint32, jobHandle windows.Handle, virtRootPath *uint16, sid *windows.SID, bufferSize *uint32, outBuffer *byte) (hr error) { 48 | hr = procBfGetMappings.Find() 49 | if hr != nil { 50 | return 51 | } 52 | r0, _, _ := syscall.SyscallN(procBfGetMappings.Addr(), uintptr(flags), uintptr(jobHandle), uintptr(unsafe.Pointer(virtRootPath)), uintptr(unsafe.Pointer(sid)), uintptr(unsafe.Pointer(bufferSize)), uintptr(unsafe.Pointer(outBuffer))) 53 | if int32(r0) < 0 { 54 | if r0&0x1fff0000 == 0x00070000 { 55 | r0 &= 0xffff 56 | } 57 | hr = syscall.Errno(r0) 58 | } 59 | return 60 | } 61 | 62 | func bfRemoveMapping(jobHandle windows.Handle, virtRootPath string) (hr error) { 63 | var _p0 *uint16 64 | _p0, hr = syscall.UTF16PtrFromString(virtRootPath) 65 | if hr != nil { 66 | return 67 | } 68 | return _bfRemoveMapping(jobHandle, _p0) 69 | } 70 | 71 | func _bfRemoveMapping(jobHandle windows.Handle, virtRootPath *uint16) (hr error) { 72 | hr = procBfRemoveMapping.Find() 73 | if hr != nil { 74 | return 75 | } 76 | r0, _, _ := syscall.SyscallN(procBfRemoveMapping.Addr(), uintptr(jobHandle), uintptr(unsafe.Pointer(virtRootPath))) 77 | if int32(r0) < 0 { 78 | if r0&0x1fff0000 == 0x00070000 { 79 | r0 &= 0xffff 80 | } 81 | hr = syscall.Errno(r0) 82 | } 83 | return 84 | } 85 | 86 | func bfSetupFilter(jobHandle windows.Handle, flags uint32, virtRootPath string, virtTargetPath string, virtExceptions **uint16, virtExceptionPathCount uint32) (hr error) { 87 | var _p0 *uint16 88 | _p0, hr = syscall.UTF16PtrFromString(virtRootPath) 89 | if hr != nil { 90 | return 91 | } 92 | var _p1 *uint16 93 | _p1, hr = syscall.UTF16PtrFromString(virtTargetPath) 94 | if hr != nil { 95 | return 96 | } 97 | return _bfSetupFilter(jobHandle, flags, _p0, _p1, virtExceptions, virtExceptionPathCount) 98 | } 99 | 100 | func _bfSetupFilter(jobHandle windows.Handle, flags uint32, virtRootPath *uint16, virtTargetPath *uint16, virtExceptions **uint16, virtExceptionPathCount uint32) (hr error) { 101 | hr = procBfSetupFilter.Find() 102 | if hr != nil { 103 | return 104 | } 105 | r0, _, _ := syscall.SyscallN(procBfSetupFilter.Addr(), uintptr(jobHandle), uintptr(flags), uintptr(unsafe.Pointer(virtRootPath)), uintptr(unsafe.Pointer(virtTargetPath)), uintptr(unsafe.Pointer(virtExceptions)), uintptr(virtExceptionPathCount)) 106 | if int32(r0) < 0 { 107 | if r0&0x1fff0000 == 0x00070000 { 108 | r0 &= 0xffff 109 | } 110 | hr = syscall.Errno(r0) 111 | } 112 | return 113 | } 114 | -------------------------------------------------------------------------------- /ea.go: -------------------------------------------------------------------------------- 1 | package winio 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | ) 8 | 9 | type fileFullEaInformation struct { 10 | NextEntryOffset uint32 11 | Flags uint8 12 | NameLength uint8 13 | ValueLength uint16 14 | } 15 | 16 | var ( 17 | fileFullEaInformationSize = binary.Size(&fileFullEaInformation{}) 18 | 19 | errInvalidEaBuffer = errors.New("invalid extended attribute buffer") 20 | errEaNameTooLarge = errors.New("extended attribute name too large") 21 | errEaValueTooLarge = errors.New("extended attribute value too large") 22 | ) 23 | 24 | // ExtendedAttribute represents a single Windows EA. 25 | type ExtendedAttribute struct { 26 | Name string 27 | Value []byte 28 | Flags uint8 29 | } 30 | 31 | func parseEa(b []byte) (ea ExtendedAttribute, nb []byte, err error) { 32 | var info fileFullEaInformation 33 | err = binary.Read(bytes.NewReader(b), binary.LittleEndian, &info) 34 | if err != nil { 35 | err = errInvalidEaBuffer 36 | return ea, nb, err 37 | } 38 | 39 | nameOffset := fileFullEaInformationSize 40 | nameLen := int(info.NameLength) 41 | valueOffset := nameOffset + int(info.NameLength) + 1 42 | valueLen := int(info.ValueLength) 43 | nextOffset := int(info.NextEntryOffset) 44 | if valueLen+valueOffset > len(b) || nextOffset < 0 || nextOffset > len(b) { 45 | err = errInvalidEaBuffer 46 | return ea, nb, err 47 | } 48 | 49 | ea.Name = string(b[nameOffset : nameOffset+nameLen]) 50 | ea.Value = b[valueOffset : valueOffset+valueLen] 51 | ea.Flags = info.Flags 52 | if info.NextEntryOffset != 0 { 53 | nb = b[info.NextEntryOffset:] 54 | } 55 | return ea, nb, err 56 | } 57 | 58 | // DecodeExtendedAttributes decodes a list of EAs from a FILE_FULL_EA_INFORMATION 59 | // buffer retrieved from BackupRead, ZwQueryEaFile, etc. 60 | func DecodeExtendedAttributes(b []byte) (eas []ExtendedAttribute, err error) { 61 | for len(b) != 0 { 62 | ea, nb, err := parseEa(b) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | eas = append(eas, ea) 68 | b = nb 69 | } 70 | return eas, err 71 | } 72 | 73 | func writeEa(buf *bytes.Buffer, ea *ExtendedAttribute, last bool) error { 74 | if int(uint8(len(ea.Name))) != len(ea.Name) { 75 | return errEaNameTooLarge 76 | } 77 | if int(uint16(len(ea.Value))) != len(ea.Value) { 78 | return errEaValueTooLarge 79 | } 80 | entrySize := uint32(fileFullEaInformationSize + len(ea.Name) + 1 + len(ea.Value)) 81 | withPadding := (entrySize + 3) &^ 3 82 | nextOffset := uint32(0) 83 | if !last { 84 | nextOffset = withPadding 85 | } 86 | info := fileFullEaInformation{ 87 | NextEntryOffset: nextOffset, 88 | Flags: ea.Flags, 89 | NameLength: uint8(len(ea.Name)), 90 | ValueLength: uint16(len(ea.Value)), 91 | } 92 | 93 | err := binary.Write(buf, binary.LittleEndian, &info) 94 | if err != nil { 95 | return err 96 | } 97 | 98 | _, err = buf.Write([]byte(ea.Name)) 99 | if err != nil { 100 | return err 101 | } 102 | 103 | err = buf.WriteByte(0) 104 | if err != nil { 105 | return err 106 | } 107 | 108 | _, err = buf.Write(ea.Value) 109 | if err != nil { 110 | return err 111 | } 112 | 113 | _, err = buf.Write([]byte{0, 0, 0}[0 : withPadding-entrySize]) 114 | if err != nil { 115 | return err 116 | } 117 | 118 | return nil 119 | } 120 | 121 | // EncodeExtendedAttributes encodes a list of EAs into a FILE_FULL_EA_INFORMATION 122 | // buffer for use with BackupWrite, ZwSetEaFile, etc. 123 | func EncodeExtendedAttributes(eas []ExtendedAttribute) ([]byte, error) { 124 | var buf bytes.Buffer 125 | for i := range eas { 126 | last := false 127 | if i == len(eas)-1 { 128 | last = true 129 | } 130 | 131 | err := writeEa(&buf, &eas[i], last) 132 | if err != nil { 133 | return nil, err 134 | } 135 | } 136 | return buf.Bytes(), nil 137 | } 138 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-winio [![Build Status](https://github.com/microsoft/go-winio/actions/workflows/ci.yml/badge.svg)](https://github.com/microsoft/go-winio/actions/workflows/ci.yml) 2 | 3 | This repository contains utilities for efficiently performing Win32 IO operations in 4 | Go. Currently, this is focused on accessing named pipes and other file handles, and 5 | for using named pipes as a net transport. 6 | 7 | This code relies on IO completion ports to avoid blocking IO on system threads, allowing Go 8 | to reuse the thread to schedule another goroutine. This limits support to Windows Vista and 9 | newer operating systems. This is similar to the implementation of network sockets in Go's net 10 | package. 11 | 12 | Please see the LICENSE file for licensing information. 13 | 14 | ## Contributing 15 | 16 | This project welcomes contributions and suggestions. 17 | Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that 18 | you have the right to, and actually do, grant us the rights to use your contribution. 19 | For details, visit [Microsoft CLA](https://cla.microsoft.com). 20 | 21 | When you submit a pull request, a CLA-bot will automatically determine whether you need to 22 | provide a CLA and decorate the PR appropriately (e.g., label, comment). 23 | Simply follow the instructions provided by the bot. 24 | You will only need to do this once across all repos using our CLA. 25 | 26 | Additionally, the pull request pipeline requires the following steps to be performed before 27 | mergining. 28 | 29 | ### Code Sign-Off 30 | 31 | We require that contributors sign their commits using [`git commit --signoff`][git-commit-s] 32 | to certify they either authored the work themselves or otherwise have permission to use it in this project. 33 | 34 | A range of commits can be signed off using [`git rebase --signoff`][git-rebase-s]. 35 | 36 | Please see [the developer certificate](https://developercertificate.org) for more info, 37 | as well as to make sure that you can attest to the rules listed. 38 | Our CI uses the DCO Github app to ensure that all commits in a given PR are signed-off. 39 | 40 | ### Linting 41 | 42 | Code must pass a linting stage, which uses [`golangci-lint`][lint]. 43 | The linting settings are stored in [`.golangci.yaml`](./.golangci.yaml), and can be run 44 | automatically with VSCode by adding the following to your workspace or folder settings: 45 | 46 | ```json 47 | "go.lintTool": "golangci-lint", 48 | "go.lintOnSave": "package", 49 | ``` 50 | 51 | Additional editor [integrations options are also available][lint-ide]. 52 | 53 | Alternatively, `golangci-lint` can be [installed locally][lint-install] and run from the repo root: 54 | 55 | ```shell 56 | # use . or specify a path to only lint a package 57 | # to show all lint errors, use flags "--max-issues-per-linter=0 --max-same-issues=0" 58 | > golangci-lint run ./... 59 | ``` 60 | 61 | ### Go Generate 62 | 63 | The pipeline checks that auto-generated code, via `go generate`, are up to date. 64 | 65 | This can be done for the entire repo: 66 | 67 | ```shell 68 | > go generate ./... 69 | ``` 70 | 71 | ## Code of Conduct 72 | 73 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 74 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 75 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 76 | 77 | ## Special Thanks 78 | 79 | Thanks to [natefinch][natefinch] for the inspiration for this library. 80 | See [npipe](https://github.com/natefinch/npipe) for another named pipe implementation. 81 | 82 | [lint]: https://golangci-lint.run/ 83 | [lint-ide]: https://golangci-lint.run/usage/integrations/#editor-integration 84 | [lint-install]: https://golangci-lint.run/usage/install/#local-installation 85 | 86 | [git-commit-s]: https://git-scm.com/docs/git-commit#Documentation/git-commit.txt--s 87 | [git-rebase-s]: https://git-scm.com/docs/git-rebase#Documentation/git-rebase.txt---signoff 88 | 89 | [natefinch]: https://github.com/natefinch 90 | -------------------------------------------------------------------------------- /fileinfo.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package winio 4 | 5 | import ( 6 | "os" 7 | "runtime" 8 | "unsafe" 9 | 10 | "golang.org/x/sys/windows" 11 | ) 12 | 13 | // FileBasicInfo contains file access time and file attributes information. 14 | type FileBasicInfo struct { 15 | CreationTime, LastAccessTime, LastWriteTime, ChangeTime windows.Filetime 16 | FileAttributes uint32 17 | _ uint32 // padding 18 | } 19 | 20 | // alignedFileBasicInfo is a FileBasicInfo, but aligned to uint64 by containing 21 | // uint64 rather than windows.Filetime. Filetime contains two uint32s. uint64 22 | // alignment is necessary to pass this as FILE_BASIC_INFO. 23 | type alignedFileBasicInfo struct { 24 | CreationTime, LastAccessTime, LastWriteTime, ChangeTime uint64 25 | FileAttributes uint32 26 | _ uint32 // padding 27 | } 28 | 29 | // GetFileBasicInfo retrieves times and attributes for a file. 30 | func GetFileBasicInfo(f *os.File) (*FileBasicInfo, error) { 31 | bi := &alignedFileBasicInfo{} 32 | if err := windows.GetFileInformationByHandleEx( 33 | windows.Handle(f.Fd()), 34 | windows.FileBasicInfo, 35 | (*byte)(unsafe.Pointer(bi)), 36 | uint32(unsafe.Sizeof(*bi)), 37 | ); err != nil { 38 | return nil, &os.PathError{Op: "GetFileInformationByHandleEx", Path: f.Name(), Err: err} 39 | } 40 | runtime.KeepAlive(f) 41 | // Reinterpret the alignedFileBasicInfo as a FileBasicInfo so it matches the 42 | // public API of this module. The data may be unnecessarily aligned. 43 | return (*FileBasicInfo)(unsafe.Pointer(bi)), nil 44 | } 45 | 46 | // SetFileBasicInfo sets times and attributes for a file. 47 | func SetFileBasicInfo(f *os.File, bi *FileBasicInfo) error { 48 | // Create an alignedFileBasicInfo based on a FileBasicInfo. The copy is 49 | // suitable to pass to GetFileInformationByHandleEx. 50 | biAligned := *(*alignedFileBasicInfo)(unsafe.Pointer(bi)) 51 | if err := windows.SetFileInformationByHandle( 52 | windows.Handle(f.Fd()), 53 | windows.FileBasicInfo, 54 | (*byte)(unsafe.Pointer(&biAligned)), 55 | uint32(unsafe.Sizeof(biAligned)), 56 | ); err != nil { 57 | return &os.PathError{Op: "SetFileInformationByHandle", Path: f.Name(), Err: err} 58 | } 59 | runtime.KeepAlive(f) 60 | return nil 61 | } 62 | 63 | // FileStandardInfo contains extended information for the file. 64 | // FILE_STANDARD_INFO in WinBase.h 65 | // https://docs.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-file_standard_info 66 | type FileStandardInfo struct { 67 | AllocationSize, EndOfFile int64 68 | NumberOfLinks uint32 69 | DeletePending, Directory bool 70 | } 71 | 72 | // GetFileStandardInfo retrieves ended information for the file. 73 | func GetFileStandardInfo(f *os.File) (*FileStandardInfo, error) { 74 | si := &FileStandardInfo{} 75 | if err := windows.GetFileInformationByHandleEx(windows.Handle(f.Fd()), 76 | windows.FileStandardInfo, 77 | (*byte)(unsafe.Pointer(si)), 78 | uint32(unsafe.Sizeof(*si))); err != nil { 79 | return nil, &os.PathError{Op: "GetFileInformationByHandleEx", Path: f.Name(), Err: err} 80 | } 81 | runtime.KeepAlive(f) 82 | return si, nil 83 | } 84 | 85 | // FileIDInfo contains the volume serial number and file ID for a file. This pair should be 86 | // unique on a system. 87 | type FileIDInfo struct { 88 | VolumeSerialNumber uint64 89 | FileID [16]byte 90 | } 91 | 92 | // GetFileID retrieves the unique (volume, file ID) pair for a file. 93 | func GetFileID(f *os.File) (*FileIDInfo, error) { 94 | fileID := &FileIDInfo{} 95 | if err := windows.GetFileInformationByHandleEx( 96 | windows.Handle(f.Fd()), 97 | windows.FileIdInfo, 98 | (*byte)(unsafe.Pointer(fileID)), 99 | uint32(unsafe.Sizeof(*fileID)), 100 | ); err != nil { 101 | return nil, &os.PathError{Op: "GetFileInformationByHandleEx", Path: f.Name(), Err: err} 102 | } 103 | runtime.KeepAlive(f) 104 | return fileID, nil 105 | } 106 | -------------------------------------------------------------------------------- /internal/stringbuffer/wstring.go: -------------------------------------------------------------------------------- 1 | package stringbuffer 2 | 3 | import ( 4 | "sync" 5 | "unicode/utf16" 6 | ) 7 | 8 | // TODO: worth exporting and using in mkwinsyscall? 9 | 10 | // Uint16BufferSize is the buffer size in the pool, chosen somewhat arbitrarily to accommodate 11 | // large path strings: 12 | // MAX_PATH (260) + size of volume GUID prefix (49) + null terminator = 310. 13 | const MinWStringCap = 310 14 | 15 | // use *[]uint16 since []uint16 creates an extra allocation where the slice header 16 | // is copied to heap and then referenced via pointer in the interface header that sync.Pool 17 | // stores. 18 | var pathPool = sync.Pool{ // if go1.18+ adds Pool[T], use that to store []uint16 directly 19 | New: func() interface{} { 20 | b := make([]uint16, MinWStringCap) 21 | return &b 22 | }, 23 | } 24 | 25 | func newBuffer() []uint16 { return *(pathPool.Get().(*[]uint16)) } 26 | 27 | // freeBuffer copies the slice header data, and puts a pointer to that in the pool. 28 | // This avoids taking a pointer to the slice header in WString, which can be set to nil. 29 | func freeBuffer(b []uint16) { pathPool.Put(&b) } 30 | 31 | // WString is a wide string buffer ([]uint16) meant for storing UTF-16 encoded strings 32 | // for interacting with Win32 APIs. 33 | // Sizes are specified as uint32 and not int. 34 | // 35 | // It is not thread safe. 36 | type WString struct { 37 | // type-def allows casting to []uint16 directly, use struct to prevent that and allow adding fields in the future. 38 | 39 | // raw buffer 40 | b []uint16 41 | } 42 | 43 | // NewWString returns a [WString] allocated from a shared pool with an 44 | // initial capacity of at least [MinWStringCap]. 45 | // Since the buffer may have been previously used, its contents are not guaranteed to be empty. 46 | // 47 | // The buffer should be freed via [WString.Free] 48 | func NewWString() *WString { 49 | return &WString{ 50 | b: newBuffer(), 51 | } 52 | } 53 | 54 | func (b *WString) Free() { 55 | if b.empty() { 56 | return 57 | } 58 | freeBuffer(b.b) 59 | b.b = nil 60 | } 61 | 62 | // ResizeTo grows the buffer to at least c and returns the new capacity, freeing the 63 | // previous buffer back into pool. 64 | func (b *WString) ResizeTo(c uint32) uint32 { 65 | // already sufficient (or n is 0) 66 | if c <= b.Cap() { 67 | return b.Cap() 68 | } 69 | 70 | if c <= MinWStringCap { 71 | c = MinWStringCap 72 | } 73 | // allocate at-least double buffer size, as is done in [bytes.Buffer] and other places 74 | if c <= 2*b.Cap() { 75 | c = 2 * b.Cap() 76 | } 77 | 78 | b2 := make([]uint16, c) 79 | if !b.empty() { 80 | copy(b2, b.b) 81 | freeBuffer(b.b) 82 | } 83 | b.b = b2 84 | return c 85 | } 86 | 87 | // Buffer returns the underlying []uint16 buffer. 88 | func (b *WString) Buffer() []uint16 { 89 | if b.empty() { 90 | return nil 91 | } 92 | return b.b 93 | } 94 | 95 | // Pointer returns a pointer to the first uint16 in the buffer. 96 | // If the [WString.Free] has already been called, the pointer will be nil. 97 | func (b *WString) Pointer() *uint16 { 98 | if b.empty() { 99 | return nil 100 | } 101 | return &b.b[0] 102 | } 103 | 104 | // String returns the returns the UTF-8 encoding of the UTF-16 string in the buffer. 105 | // 106 | // It assumes that the data is null-terminated. 107 | func (b *WString) String() string { 108 | // Using [windows.UTF16ToString] would require importing "golang.org/x/sys/windows" 109 | // and would make this code Windows-only, which makes no sense. 110 | // So copy UTF16ToString code into here. 111 | // If other windows-specific code is added, switch to [windows.UTF16ToString] 112 | 113 | s := b.b 114 | for i, v := range s { 115 | if v == 0 { 116 | s = s[:i] 117 | break 118 | } 119 | } 120 | return string(utf16.Decode(s)) 121 | } 122 | 123 | // Cap returns the underlying buffer capacity. 124 | func (b *WString) Cap() uint32 { 125 | if b.empty() { 126 | return 0 127 | } 128 | return b.cap() 129 | } 130 | 131 | func (b *WString) cap() uint32 { return uint32(cap(b.b)) } 132 | func (b *WString) empty() bool { return b == nil || b.cap() == 0 } 133 | -------------------------------------------------------------------------------- /reparse.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package winio 4 | 5 | import ( 6 | "bytes" 7 | "encoding/binary" 8 | "fmt" 9 | "strings" 10 | "unicode/utf16" 11 | "unsafe" 12 | ) 13 | 14 | const ( 15 | reparseTagMountPoint = 0xA0000003 16 | reparseTagSymlink = 0xA000000C 17 | ) 18 | 19 | type reparseDataBuffer struct { 20 | ReparseTag uint32 21 | ReparseDataLength uint16 22 | Reserved uint16 23 | SubstituteNameOffset uint16 24 | SubstituteNameLength uint16 25 | PrintNameOffset uint16 26 | PrintNameLength uint16 27 | } 28 | 29 | // ReparsePoint describes a Win32 symlink or mount point. 30 | type ReparsePoint struct { 31 | Target string 32 | IsMountPoint bool 33 | } 34 | 35 | // UnsupportedReparsePointError is returned when trying to decode a non-symlink or 36 | // mount point reparse point. 37 | type UnsupportedReparsePointError struct { 38 | Tag uint32 39 | } 40 | 41 | func (e *UnsupportedReparsePointError) Error() string { 42 | return fmt.Sprintf("unsupported reparse point %x", e.Tag) 43 | } 44 | 45 | // DecodeReparsePoint decodes a Win32 REPARSE_DATA_BUFFER structure containing either a symlink 46 | // or a mount point. 47 | func DecodeReparsePoint(b []byte) (*ReparsePoint, error) { 48 | tag := binary.LittleEndian.Uint32(b[0:4]) 49 | return DecodeReparsePointData(tag, b[8:]) 50 | } 51 | 52 | func DecodeReparsePointData(tag uint32, b []byte) (*ReparsePoint, error) { 53 | isMountPoint := false 54 | switch tag { 55 | case reparseTagMountPoint: 56 | isMountPoint = true 57 | case reparseTagSymlink: 58 | default: 59 | return nil, &UnsupportedReparsePointError{tag} 60 | } 61 | nameOffset := 8 + binary.LittleEndian.Uint16(b[4:6]) 62 | if !isMountPoint { 63 | nameOffset += 4 64 | } 65 | nameLength := binary.LittleEndian.Uint16(b[6:8]) 66 | name := make([]uint16, nameLength/2) 67 | err := binary.Read(bytes.NewReader(b[nameOffset:nameOffset+nameLength]), binary.LittleEndian, &name) 68 | if err != nil { 69 | return nil, err 70 | } 71 | return &ReparsePoint{string(utf16.Decode(name)), isMountPoint}, nil 72 | } 73 | 74 | func isDriveLetter(c byte) bool { 75 | return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') 76 | } 77 | 78 | // EncodeReparsePoint encodes a Win32 REPARSE_DATA_BUFFER structure describing a symlink or 79 | // mount point. 80 | func EncodeReparsePoint(rp *ReparsePoint) []byte { 81 | // Generate an NT path and determine if this is a relative path. 82 | var ntTarget string 83 | relative := false 84 | if strings.HasPrefix(rp.Target, `\\?\`) { 85 | ntTarget = `\??\` + rp.Target[4:] 86 | } else if strings.HasPrefix(rp.Target, `\\`) { 87 | ntTarget = `\??\UNC\` + rp.Target[2:] 88 | } else if len(rp.Target) >= 2 && isDriveLetter(rp.Target[0]) && rp.Target[1] == ':' { 89 | ntTarget = `\??\` + rp.Target 90 | } else { 91 | ntTarget = rp.Target 92 | relative = true 93 | } 94 | 95 | // The paths must be NUL-terminated even though they are counted strings. 96 | target16 := utf16.Encode([]rune(rp.Target + "\x00")) 97 | ntTarget16 := utf16.Encode([]rune(ntTarget + "\x00")) 98 | 99 | size := int(unsafe.Sizeof(reparseDataBuffer{})) - 8 100 | size += len(ntTarget16)*2 + len(target16)*2 101 | 102 | tag := uint32(reparseTagMountPoint) 103 | if !rp.IsMountPoint { 104 | tag = reparseTagSymlink 105 | size += 4 // Add room for symlink flags 106 | } 107 | 108 | data := reparseDataBuffer{ 109 | ReparseTag: tag, 110 | ReparseDataLength: uint16(size), 111 | SubstituteNameOffset: 0, 112 | SubstituteNameLength: uint16((len(ntTarget16) - 1) * 2), 113 | PrintNameOffset: uint16(len(ntTarget16) * 2), 114 | PrintNameLength: uint16((len(target16) - 1) * 2), 115 | } 116 | 117 | var b bytes.Buffer 118 | _ = binary.Write(&b, binary.LittleEndian, &data) 119 | if !rp.IsMountPoint { 120 | flags := uint32(0) 121 | if relative { 122 | flags |= 1 123 | } 124 | _ = binary.Write(&b, binary.LittleEndian, flags) 125 | } 126 | 127 | _ = binary.Write(&b, binary.LittleEndian, ntTarget16) 128 | _ = binary.Write(&b, binary.LittleEndian, target16) 129 | return b.Bytes() 130 | } 131 | -------------------------------------------------------------------------------- /pkg/etw/zsyscall_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | // Code generated by 'go generate' using "github.com/Microsoft/go-winio/tools/mkwinsyscall"; DO NOT EDIT. 4 | 5 | package etw 6 | 7 | import ( 8 | "syscall" 9 | "unsafe" 10 | 11 | "golang.org/x/sys/windows" 12 | ) 13 | 14 | var _ unsafe.Pointer 15 | 16 | // Do the interface allocations only once for common 17 | // Errno values. 18 | const ( 19 | errnoERROR_IO_PENDING = 997 20 | ) 21 | 22 | var ( 23 | errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING) 24 | errERROR_EINVAL error = syscall.EINVAL 25 | ) 26 | 27 | // errnoErr returns common boxed Errno values, to prevent 28 | // allocations at runtime. 29 | func errnoErr(e syscall.Errno) error { 30 | switch e { 31 | case 0: 32 | return errERROR_EINVAL 33 | case errnoERROR_IO_PENDING: 34 | return errERROR_IO_PENDING 35 | } 36 | return e 37 | } 38 | 39 | var ( 40 | modadvapi32 = windows.NewLazySystemDLL("advapi32.dll") 41 | 42 | procEventRegister = modadvapi32.NewProc("EventRegister") 43 | procEventSetInformation = modadvapi32.NewProc("EventSetInformation") 44 | procEventUnregister = modadvapi32.NewProc("EventUnregister") 45 | procEventWriteTransfer = modadvapi32.NewProc("EventWriteTransfer") 46 | ) 47 | 48 | func eventRegister(providerId *windows.GUID, callback uintptr, callbackContext uintptr, providerHandle *providerHandle) (win32err error) { 49 | r0, _, _ := syscall.SyscallN(procEventRegister.Addr(), uintptr(unsafe.Pointer(providerId)), uintptr(callback), uintptr(callbackContext), uintptr(unsafe.Pointer(providerHandle))) 50 | if r0 != 0 { 51 | win32err = syscall.Errno(r0) 52 | } 53 | return 54 | } 55 | 56 | func eventSetInformation_64(providerHandle providerHandle, class eventInfoClass, information uintptr, length uint32) (win32err error) { 57 | r0, _, _ := syscall.SyscallN(procEventSetInformation.Addr(), uintptr(providerHandle), uintptr(class), uintptr(information), uintptr(length)) 58 | if r0 != 0 { 59 | win32err = syscall.Errno(r0) 60 | } 61 | return 62 | } 63 | 64 | func eventSetInformation_32(providerHandle_low uint32, providerHandle_high uint32, class eventInfoClass, information uintptr, length uint32) (win32err error) { 65 | r0, _, _ := syscall.SyscallN(procEventSetInformation.Addr(), uintptr(providerHandle_low), uintptr(providerHandle_high), uintptr(class), uintptr(information), uintptr(length)) 66 | if r0 != 0 { 67 | win32err = syscall.Errno(r0) 68 | } 69 | return 70 | } 71 | 72 | func eventUnregister_64(providerHandle providerHandle) (win32err error) { 73 | r0, _, _ := syscall.SyscallN(procEventUnregister.Addr(), uintptr(providerHandle)) 74 | if r0 != 0 { 75 | win32err = syscall.Errno(r0) 76 | } 77 | return 78 | } 79 | 80 | func eventUnregister_32(providerHandle_low uint32, providerHandle_high uint32) (win32err error) { 81 | r0, _, _ := syscall.SyscallN(procEventUnregister.Addr(), uintptr(providerHandle_low), uintptr(providerHandle_high)) 82 | if r0 != 0 { 83 | win32err = syscall.Errno(r0) 84 | } 85 | return 86 | } 87 | 88 | func eventWriteTransfer_64(providerHandle providerHandle, descriptor *eventDescriptor, activityID *windows.GUID, relatedActivityID *windows.GUID, dataDescriptorCount uint32, dataDescriptors *eventDataDescriptor) (win32err error) { 89 | r0, _, _ := syscall.SyscallN(procEventWriteTransfer.Addr(), uintptr(providerHandle), uintptr(unsafe.Pointer(descriptor)), uintptr(unsafe.Pointer(activityID)), uintptr(unsafe.Pointer(relatedActivityID)), uintptr(dataDescriptorCount), uintptr(unsafe.Pointer(dataDescriptors))) 90 | if r0 != 0 { 91 | win32err = syscall.Errno(r0) 92 | } 93 | return 94 | } 95 | 96 | func eventWriteTransfer_32(providerHandle_low uint32, providerHandle_high uint32, descriptor *eventDescriptor, activityID *windows.GUID, relatedActivityID *windows.GUID, dataDescriptorCount uint32, dataDescriptors *eventDataDescriptor) (win32err error) { 97 | r0, _, _ := syscall.SyscallN(procEventWriteTransfer.Addr(), uintptr(providerHandle_low), uintptr(providerHandle_high), uintptr(unsafe.Pointer(descriptor)), uintptr(unsafe.Pointer(activityID)), uintptr(unsafe.Pointer(relatedActivityID)), uintptr(dataDescriptorCount), uintptr(unsafe.Pointer(dataDescriptors))) 98 | if r0 != 0 { 99 | win32err = syscall.Errno(r0) 100 | } 101 | return 102 | } 103 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | - pull_request 5 | 6 | env: 7 | GO_VERSION: "oldstable" 8 | GOTESTSUM_VERSION: "latest" 9 | GOLANGCILINT_VERSION: "latest" 10 | 11 | jobs: 12 | lint: 13 | name: Lint 14 | runs-on: windows-2022 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | with: 19 | show-progress: false 20 | 21 | - name: Install go 22 | uses: actions/setup-go@v5 23 | with: 24 | go-version: ${{ env.GO_VERSION }} 25 | cache: false 26 | 27 | - name: Run golangci-lint 28 | uses: golangci/golangci-lint-action@v4 29 | with: 30 | version: ${{ env.GOLANGCILINT_VERSION }} 31 | args: >- 32 | --verbose 33 | --timeout=5m 34 | --config=.golangci.yml 35 | --max-issues-per-linter=0 36 | --max-same-issues=0 37 | --modules-download-mode=readonly 38 | 39 | go-generate: 40 | name: Go Generate 41 | runs-on: windows-2022 42 | steps: 43 | - name: Checkout 44 | uses: actions/checkout@v4 45 | with: 46 | show-progress: false 47 | 48 | - name: Install go 49 | uses: actions/setup-go@v5 50 | with: 51 | go-version: ${{ env.GO_VERSION }} 52 | # don't really need to cache Go packages, since go generate doesn't require much. 53 | # otherwise, the cache used in the `test` stage will be (basically) empty. 54 | cache: false 55 | 56 | - name: Run go generate 57 | shell: pwsh 58 | run: | 59 | Write-Output "::group::go generate" 60 | go generate -x ./... 61 | Write-Output "::endgroup::" 62 | if ($LASTEXITCODE -ne 0) { 63 | Write-Output "::error title=Go Generate::Error running go generate." 64 | exit $LASTEXITCODE 65 | } 66 | 67 | - name: Diff 68 | shell: pwsh 69 | run: | 70 | git add -N . 71 | Write-Output "::group::git diff" 72 | git diff --stat --exit-code 73 | Write-Output "::endgroup::" 74 | if ($LASTEXITCODE -ne 0) { 75 | Write-Output "::error ::Generated files are not up to date. Please run ``go generate ./...``." 76 | exit $LASTEXITCODE 77 | } 78 | 79 | test: 80 | name: Run Tests 81 | needs: 82 | - go-generate 83 | runs-on: ${{ matrix.os }} 84 | strategy: 85 | fail-fast: false 86 | matrix: 87 | os: [windows-2019, windows-2022, ubuntu-latest] 88 | steps: 89 | - name: Checkout 90 | uses: actions/checkout@v4 91 | with: 92 | show-progress: false 93 | 94 | - name: Install go 95 | uses: actions/setup-go@v5 96 | with: 97 | go-version: ${{ env.GO_VERSION }} 98 | 99 | # avoid needing to download packages during test runs 100 | - name: Pre-fill Module Cache 101 | run: go mod download -x 102 | 103 | - name: Install gotestsum 104 | run: go install gotest.tools/gotestsum@${{ env.GOTESTSUM_VERSION }} 105 | 106 | - name: Test repo 107 | shell: pwsh 108 | run: | 109 | # `go test -race` requires mingw on windows, and there is some weirdness on windows 2019 110 | # which causes tests to fail with `"exit status 0xc0000139"`. 111 | # Even trying to update mingw with choco still fails. 112 | # 113 | # see: https://go.dev/doc/articles/race_detector#Requirements 114 | 115 | if ( '${{ matrix.os }}' -ne 'windows-2019' ) { 116 | $race = '-race' 117 | } 118 | 119 | gotestsum --format standard-verbose --debug -- -gcflags=all=-d=checkptr $race -v ./... 120 | 121 | # !NOTE: 122 | # Fuzzing cannot be run across multiple packages, (ie, `go -fuzz "^Fuzz" ./...` fails). 123 | # If new fuzzing tests are added, exec additional runs for each package. 124 | - name: Fuzz root package 125 | run: gotestsum --format standard-verbose --debug -- -run "^#" -fuzztime 1m -fuzz "^Fuzz" 126 | 127 | build: 128 | name: Build Repo 129 | needs: 130 | - test 131 | runs-on: "windows-2022" 132 | steps: 133 | - name: Checkout 134 | uses: actions/checkout@v4 135 | with: 136 | show-progress: false 137 | 138 | - name: Install go 139 | uses: actions/setup-go@v5 140 | with: 141 | go-version: ${{ env.GO_VERSION }} 142 | 143 | - run: go build ./pkg/etw/sample/ 144 | - run: go build ./tools/etw-provider-gen/ 145 | - run: go build ./tools/mkwinsyscall/ 146 | - run: go build ./wim/validate/ 147 | -------------------------------------------------------------------------------- /sd.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package winio 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "unsafe" 9 | 10 | "golang.org/x/sys/windows" 11 | ) 12 | 13 | //sys lookupAccountName(systemName *uint16, accountName string, sid *byte, sidSize *uint32, refDomain *uint16, refDomainSize *uint32, sidNameUse *uint32) (err error) = advapi32.LookupAccountNameW 14 | //sys lookupAccountSid(systemName *uint16, sid *byte, name *uint16, nameSize *uint32, refDomain *uint16, refDomainSize *uint32, sidNameUse *uint32) (err error) = advapi32.LookupAccountSidW 15 | //sys convertSidToStringSid(sid *byte, str **uint16) (err error) = advapi32.ConvertSidToStringSidW 16 | //sys convertStringSidToSid(str *uint16, sid **byte) (err error) = advapi32.ConvertStringSidToSidW 17 | 18 | type AccountLookupError struct { 19 | Name string 20 | Err error 21 | } 22 | 23 | func (e *AccountLookupError) Error() string { 24 | if e.Name == "" { 25 | return "lookup account: empty account name specified" 26 | } 27 | var s string 28 | switch { 29 | case errors.Is(e.Err, windows.ERROR_INVALID_SID): 30 | s = "the security ID structure is invalid" 31 | case errors.Is(e.Err, windows.ERROR_NONE_MAPPED): 32 | s = "not found" 33 | default: 34 | s = e.Err.Error() 35 | } 36 | return "lookup account " + e.Name + ": " + s 37 | } 38 | 39 | func (e *AccountLookupError) Unwrap() error { return e.Err } 40 | 41 | type SddlConversionError struct { 42 | Sddl string 43 | Err error 44 | } 45 | 46 | func (e *SddlConversionError) Error() string { 47 | return "convert " + e.Sddl + ": " + e.Err.Error() 48 | } 49 | 50 | func (e *SddlConversionError) Unwrap() error { return e.Err } 51 | 52 | // LookupSidByName looks up the SID of an account by name 53 | // 54 | //revive:disable-next-line:var-naming SID, not Sid 55 | func LookupSidByName(name string) (sid string, err error) { 56 | if name == "" { 57 | return "", &AccountLookupError{name, windows.ERROR_NONE_MAPPED} 58 | } 59 | 60 | var sidSize, sidNameUse, refDomainSize uint32 61 | err = lookupAccountName(nil, name, nil, &sidSize, nil, &refDomainSize, &sidNameUse) 62 | if err != nil && err != windows.ERROR_INSUFFICIENT_BUFFER { //nolint:errorlint // err is Errno 63 | return "", &AccountLookupError{name, err} 64 | } 65 | sidBuffer := make([]byte, sidSize) 66 | refDomainBuffer := make([]uint16, refDomainSize) 67 | err = lookupAccountName(nil, name, &sidBuffer[0], &sidSize, &refDomainBuffer[0], &refDomainSize, &sidNameUse) 68 | if err != nil { 69 | return "", &AccountLookupError{name, err} 70 | } 71 | var strBuffer *uint16 72 | err = convertSidToStringSid(&sidBuffer[0], &strBuffer) 73 | if err != nil { 74 | return "", &AccountLookupError{name, err} 75 | } 76 | sid = windows.UTF16ToString((*[0xffff]uint16)(unsafe.Pointer(strBuffer))[:]) 77 | _, _ = windows.LocalFree(windows.Handle(unsafe.Pointer(strBuffer))) 78 | return sid, nil 79 | } 80 | 81 | // LookupNameBySid looks up the name of an account by SID 82 | // 83 | //revive:disable-next-line:var-naming SID, not Sid 84 | func LookupNameBySid(sid string) (name string, err error) { 85 | if sid == "" { 86 | return "", &AccountLookupError{sid, windows.ERROR_NONE_MAPPED} 87 | } 88 | 89 | sidBuffer, err := windows.UTF16PtrFromString(sid) 90 | if err != nil { 91 | return "", &AccountLookupError{sid, err} 92 | } 93 | 94 | var sidPtr *byte 95 | if err = convertStringSidToSid(sidBuffer, &sidPtr); err != nil { 96 | return "", &AccountLookupError{sid, err} 97 | } 98 | defer windows.LocalFree(windows.Handle(unsafe.Pointer(sidPtr))) //nolint:errcheck 99 | 100 | var nameSize, refDomainSize, sidNameUse uint32 101 | err = lookupAccountSid(nil, sidPtr, nil, &nameSize, nil, &refDomainSize, &sidNameUse) 102 | if err != nil && err != windows.ERROR_INSUFFICIENT_BUFFER { //nolint:errorlint // err is Errno 103 | return "", &AccountLookupError{sid, err} 104 | } 105 | 106 | nameBuffer := make([]uint16, nameSize) 107 | refDomainBuffer := make([]uint16, refDomainSize) 108 | err = lookupAccountSid(nil, sidPtr, &nameBuffer[0], &nameSize, &refDomainBuffer[0], &refDomainSize, &sidNameUse) 109 | if err != nil { 110 | return "", &AccountLookupError{sid, err} 111 | } 112 | 113 | name = windows.UTF16ToString(nameBuffer) 114 | return name, nil 115 | } 116 | 117 | func SddlToSecurityDescriptor(sddl string) ([]byte, error) { 118 | sd, err := windows.SecurityDescriptorFromString(sddl) 119 | if err != nil { 120 | return nil, &SddlConversionError{Sddl: sddl, Err: err} 121 | } 122 | b := unsafe.Slice((*byte)(unsafe.Pointer(sd)), sd.Length()) 123 | return b, nil 124 | } 125 | 126 | func SecurityDescriptorToSddl(sd []byte) (string, error) { 127 | if l := int(unsafe.Sizeof(windows.SECURITY_DESCRIPTOR{})); len(sd) < l { 128 | return "", fmt.Errorf("SecurityDescriptor (%d) smaller than expected (%d): %w", len(sd), l, windows.ERROR_INCORRECT_SIZE) 129 | } 130 | s := (*windows.SECURITY_DESCRIPTOR)(unsafe.Pointer(&sd[0])) 131 | return s.String(), nil 132 | } 133 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | enable: 3 | # style 4 | - containedctx # struct contains a context 5 | - dupl # duplicate code 6 | - errname # erorrs are named correctly 7 | - nolintlint # "//nolint" directives are properly explained 8 | - revive # golint replacement 9 | - unconvert # unnecessary conversions 10 | - wastedassign 11 | 12 | # bugs, performance, unused, etc ... 13 | - contextcheck # function uses a non-inherited context 14 | - errorlint # errors not wrapped for 1.13 15 | - exhaustive # check exhaustiveness of enum switch statements 16 | - gofmt # files are gofmt'ed 17 | - gosec # security 18 | - nilerr # returns nil even with non-nil error 19 | - thelper # test helpers without t.Helper() 20 | - unparam # unused function params 21 | 22 | issues: 23 | exclude-dirs: 24 | - pkg/etw/sample 25 | 26 | exclude-rules: 27 | # err is very often shadowed in nested scopes 28 | - linters: 29 | - govet 30 | text: '^shadow: declaration of "err" shadows declaration' 31 | 32 | # ignore long lines for skip autogen directives 33 | - linters: 34 | - revive 35 | text: "^line-length-limit: " 36 | source: "^//(go:generate|sys) " 37 | 38 | #TODO: remove after upgrading to go1.18 39 | # ignore comment spacing for nolint and sys directives 40 | - linters: 41 | - revive 42 | text: "^comment-spacings: no space between comment delimiter and comment text" 43 | source: "//(cspell:|nolint:|sys |todo)" 44 | 45 | # not on go 1.18 yet, so no any 46 | - linters: 47 | - revive 48 | text: "^use-any: since GO 1.18 'interface{}' can be replaced by 'any'" 49 | 50 | # allow unjustified ignores of error checks in defer statements 51 | - linters: 52 | - nolintlint 53 | text: "^directive `//nolint:errcheck` should provide explanation" 54 | source: '^\s*defer ' 55 | 56 | # allow unjustified ignores of error lints for io.EOF 57 | - linters: 58 | - nolintlint 59 | text: "^directive `//nolint:errorlint` should provide explanation" 60 | source: '[=|!]= io.EOF' 61 | 62 | - linters: 63 | - gosec 64 | text: "^G115: integer overflow conversion" 65 | 66 | 67 | linters-settings: 68 | exhaustive: 69 | default-signifies-exhaustive: true 70 | govet: 71 | enable-all: true 72 | disable: 73 | # struct order is often for Win32 compat 74 | # also, ignore pointer bytes/GC issues for now until performance becomes an issue 75 | - fieldalignment 76 | nolintlint: 77 | require-explanation: true 78 | require-specific: true 79 | revive: 80 | # revive is more configurable than static check, so likely the preferred alternative to static-check 81 | # (once the perf issue is solved: https://github.com/golangci/golangci-lint/issues/2997) 82 | enable-all-rules: 83 | true 84 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md 85 | rules: 86 | # rules with required arguments 87 | - name: argument-limit 88 | disabled: true 89 | - name: banned-characters 90 | disabled: true 91 | - name: cognitive-complexity 92 | disabled: true 93 | - name: cyclomatic 94 | disabled: true 95 | - name: file-header 96 | disabled: true 97 | - name: function-length 98 | disabled: true 99 | - name: function-result-limit 100 | disabled: true 101 | - name: max-public-structs 102 | disabled: true 103 | # geneally annoying rules 104 | - name: add-constant # complains about any and all strings and integers 105 | disabled: true 106 | - name: confusing-naming # we frequently use "Foo()" and "foo()" together 107 | disabled: true 108 | - name: flag-parameter # excessive, and a common idiom we use 109 | disabled: true 110 | - name: unhandled-error # warns over common fmt.Print* and io.Close; rely on errcheck instead 111 | disabled: true 112 | # general config 113 | - name: line-length-limit 114 | arguments: 115 | - 140 116 | - name: var-naming 117 | arguments: 118 | - [] 119 | - - CID 120 | - CRI 121 | - CTRD 122 | - DACL 123 | - DLL 124 | - DOS 125 | - ETW 126 | - FSCTL 127 | - GCS 128 | - GMSA 129 | - HCS 130 | - HV 131 | - IO 132 | - LCOW 133 | - LDAP 134 | - LPAC 135 | - LTSC 136 | - MMIO 137 | - NT 138 | - OCI 139 | - PMEM 140 | - PWSH 141 | - RX 142 | - SACl 143 | - SID 144 | - SMB 145 | - TX 146 | - VHD 147 | - VHDX 148 | - VMID 149 | - VPCI 150 | - WCOW 151 | - WIM 152 | -------------------------------------------------------------------------------- /pkg/etwlogrus/hook.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package etwlogrus 4 | 5 | import ( 6 | "errors" 7 | "sort" 8 | 9 | "github.com/sirupsen/logrus" 10 | 11 | "github.com/Microsoft/go-winio/pkg/etw" 12 | ) 13 | 14 | const defaultEventName = "LogrusEntry" 15 | 16 | // ErrNoProvider is returned when a hook is created without a provider being configured. 17 | var ErrNoProvider = errors.New("no ETW registered provider") 18 | 19 | // HookOpt is an option to change the behavior of the Logrus ETW hook. 20 | type HookOpt func(*Hook) error 21 | 22 | // Hook is a Logrus hook which logs received events to ETW. 23 | type Hook struct { 24 | provider *etw.Provider 25 | closeProvider bool 26 | // allows setting the entry name 27 | getName func(*logrus.Entry) string 28 | // returns additional options to add to the event 29 | getEventsOpts func(*logrus.Entry) []etw.EventOpt 30 | } 31 | 32 | // NewHook registers a new ETW provider and returns a hook to log from it. 33 | // The provider will be closed when the hook is closed. 34 | func NewHook(providerName string, opts ...HookOpt) (*Hook, error) { 35 | opts = append(opts, WithNewETWProvider(providerName)) 36 | 37 | return NewHookFromOpts(opts...) 38 | } 39 | 40 | // NewHookFromProvider creates a new hook based on an existing ETW provider. 41 | // The provider will not be closed when the hook is closed. 42 | func NewHookFromProvider(provider *etw.Provider, opts ...HookOpt) (*Hook, error) { 43 | opts = append(opts, WithExistingETWProvider(provider)) 44 | 45 | return NewHookFromOpts(opts...) 46 | } 47 | 48 | // NewHookFromOpts creates a new hook with the provided options. 49 | // An error is returned if the hook does not have a valid provider. 50 | func NewHookFromOpts(opts ...HookOpt) (*Hook, error) { 51 | h := defaultHook() 52 | 53 | for _, o := range opts { 54 | if err := o(h); err != nil { 55 | return nil, err 56 | } 57 | } 58 | return h, h.validate() 59 | } 60 | 61 | func defaultHook() *Hook { 62 | h := &Hook{} 63 | return h 64 | } 65 | 66 | func (h *Hook) validate() error { 67 | if h.provider == nil { 68 | return ErrNoProvider 69 | } 70 | return nil 71 | } 72 | 73 | // Levels returns the set of levels that this hook wants to receive log entries 74 | // for. 75 | func (*Hook) Levels() []logrus.Level { 76 | return logrus.AllLevels 77 | } 78 | 79 | var logrusToETWLevelMap = map[logrus.Level]etw.Level{ 80 | logrus.PanicLevel: etw.LevelAlways, 81 | logrus.FatalLevel: etw.LevelCritical, 82 | logrus.ErrorLevel: etw.LevelError, 83 | logrus.WarnLevel: etw.LevelWarning, 84 | logrus.InfoLevel: etw.LevelInfo, 85 | logrus.DebugLevel: etw.LevelVerbose, 86 | logrus.TraceLevel: etw.LevelVerbose, 87 | } 88 | 89 | // Fire receives each Logrus entry as it is logged, and logs it to ETW. 90 | func (h *Hook) Fire(e *logrus.Entry) error { 91 | // Logrus defines more levels than ETW typically uses, but analysis is 92 | // easiest when using a consistent set of levels across ETW providers, so we 93 | // map the Logrus levels to ETW levels. 94 | level := logrusToETWLevelMap[e.Level] 95 | if !h.provider.IsEnabledForLevel(level) { 96 | return nil 97 | } 98 | 99 | name := defaultEventName 100 | if h.getName != nil { 101 | if n := h.getName(e); n != "" { 102 | name = n 103 | } 104 | } 105 | 106 | // extra room for two more options in addition to log level to avoid repeated reallocations 107 | // if the user also provides options 108 | opts := make([]etw.EventOpt, 0, 3) 109 | opts = append(opts, etw.WithLevel(level)) 110 | if h.getEventsOpts != nil { 111 | opts = append(opts, h.getEventsOpts(e)...) 112 | } 113 | 114 | // Sort the fields by name so they are consistent in each instance 115 | // of an event. Otherwise, the fields don't line up in WPA. 116 | names := make([]string, 0, len(e.Data)) 117 | hasError := false 118 | for k := range e.Data { 119 | if k == logrus.ErrorKey { 120 | // Always put the error last because it is optional in some events. 121 | hasError = true 122 | } else { 123 | names = append(names, k) 124 | } 125 | } 126 | sort.Strings(names) 127 | 128 | // Reserve extra space for the message and time fields. 129 | fields := make([]etw.FieldOpt, 0, len(e.Data)+2) 130 | fields = append(fields, etw.StringField("Message", e.Message)) 131 | fields = append(fields, etw.Time("Time", e.Time)) 132 | for _, k := range names { 133 | fields = append(fields, etw.SmartField(k, e.Data[k])) 134 | } 135 | if hasError { 136 | fields = append(fields, etw.SmartField(logrus.ErrorKey, e.Data[logrus.ErrorKey])) 137 | } 138 | 139 | // Firing an ETW event is essentially best effort, as the event write can 140 | // fail for reasons completely out of the control of the event writer (such 141 | // as a session listening for the event having no available space in its 142 | // buffers). Therefore, we don't return the error from WriteEvent, as it is 143 | // just noise in many cases. 144 | _ = h.provider.WriteEvent(name, opts, fields) 145 | 146 | return nil 147 | } 148 | 149 | // Close cleans up the hook and closes the ETW provider. If the provder was 150 | // registered by etwlogrus, it will be closed as part of `Close`. If the 151 | // provider was passed in, it will not be closed. 152 | func (h *Hook) Close() error { 153 | if h.closeProvider { 154 | return h.provider.Close() 155 | } 156 | return nil 157 | } 158 | -------------------------------------------------------------------------------- /backup_test.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package winio 4 | 5 | import ( 6 | "io" 7 | "os" 8 | "testing" 9 | 10 | "golang.org/x/sys/windows" 11 | ) 12 | 13 | var testFileName string 14 | 15 | func TestMain(m *testing.M) { 16 | f, err := os.CreateTemp("", "tmp") 17 | if err != nil { 18 | panic(err) 19 | } 20 | testFileName = f.Name() 21 | f.Close() 22 | defer os.Remove(testFileName) 23 | os.Exit(m.Run()) 24 | } 25 | 26 | func makeTestFile(makeADS bool) error { 27 | os.Remove(testFileName) 28 | f, err := os.Create(testFileName) 29 | if err != nil { 30 | return err 31 | } 32 | defer f.Close() 33 | _, err = f.Write([]byte("testing 1 2 3\n")) 34 | if err != nil { 35 | return err 36 | } 37 | if makeADS { 38 | a, err := os.Create(testFileName + ":ads.txt") 39 | if err != nil { 40 | return err 41 | } 42 | defer a.Close() 43 | _, err = a.Write([]byte("alternate data stream\n")) 44 | if err != nil { 45 | return err 46 | } 47 | } 48 | return nil 49 | } 50 | 51 | func TestBackupRead(t *testing.T) { 52 | err := makeTestFile(true) 53 | if err != nil { 54 | t.Fatal(err) 55 | } 56 | 57 | f, err := os.Open(testFileName) 58 | if err != nil { 59 | t.Fatal(err) 60 | } 61 | defer f.Close() 62 | r := NewBackupFileReader(f, false) 63 | defer r.Close() 64 | b, err := io.ReadAll(r) 65 | if err != nil { 66 | t.Fatal(err) 67 | } 68 | if len(b) == 0 { 69 | t.Fatal("no data") 70 | } 71 | } 72 | 73 | func TestBackupStreamRead(t *testing.T) { 74 | err := makeTestFile(true) 75 | if err != nil { 76 | t.Fatal(err) 77 | } 78 | 79 | f, err := os.Open(testFileName) 80 | if err != nil { 81 | t.Fatal(err) 82 | } 83 | defer f.Close() 84 | r := NewBackupFileReader(f, false) 85 | defer r.Close() 86 | 87 | br := NewBackupStreamReader(r) 88 | gotData := false 89 | gotAltData := false 90 | for { 91 | hdr, err := br.Next() 92 | if err == io.EOF { //nolint:errorlint 93 | break 94 | } 95 | if err != nil { 96 | t.Fatal(err) 97 | } 98 | 99 | switch hdr.Id { 100 | case BackupData: 101 | if gotData { 102 | t.Fatal("duplicate data") 103 | } 104 | if hdr.Name != "" { 105 | t.Fatalf("unexpected name %s", hdr.Name) 106 | } 107 | b, err := io.ReadAll(br) 108 | if err != nil { 109 | t.Fatal(err) 110 | } 111 | if string(b) != "testing 1 2 3\n" { 112 | t.Fatalf("incorrect data %v", b) 113 | } 114 | gotData = true 115 | case BackupAlternateData: 116 | if gotAltData { 117 | t.Fatal("duplicate alt data") 118 | } 119 | if hdr.Name != ":ads.txt:$DATA" { 120 | t.Fatalf("incorrect name %s", hdr.Name) 121 | } 122 | b, err := io.ReadAll(br) 123 | if err != nil { 124 | t.Fatal(err) 125 | } 126 | if string(b) != "alternate data stream\n" { 127 | t.Fatalf("incorrect data %v", b) 128 | } 129 | gotAltData = true 130 | default: 131 | t.Fatalf("unknown stream ID %d", hdr.Id) 132 | } 133 | } 134 | if !gotData || !gotAltData { 135 | t.Fatal("missing stream") 136 | } 137 | } 138 | 139 | func TestBackupStreamWrite(t *testing.T) { 140 | f, err := os.Create(testFileName) 141 | if err != nil { 142 | t.Fatal(err) 143 | } 144 | defer f.Close() 145 | w := NewBackupFileWriter(f, false) 146 | defer w.Close() 147 | 148 | data := "testing 1 2 3\n" 149 | altData := "alternate stream\n" 150 | 151 | br := NewBackupStreamWriter(w) 152 | err = br.WriteHeader(&BackupHeader{Id: BackupData, Size: int64(len(data))}) 153 | if err != nil { 154 | t.Fatal(err) 155 | } 156 | n, err := br.Write([]byte(data)) 157 | if err != nil { 158 | t.Fatal(err) 159 | } 160 | if n != len(data) { 161 | t.Fatal("short write") 162 | } 163 | 164 | err = br.WriteHeader(&BackupHeader{Id: BackupAlternateData, Size: int64(len(altData)), Name: ":ads.txt:$DATA"}) 165 | if err != nil { 166 | t.Fatal(err) 167 | } 168 | n, err = br.Write([]byte(altData)) 169 | if err != nil { 170 | t.Fatal(err) 171 | } 172 | if n != len(altData) { 173 | t.Fatal("short write") 174 | } 175 | 176 | f.Close() 177 | 178 | b, err := os.ReadFile(testFileName) 179 | if err != nil { 180 | t.Fatal(err) 181 | } 182 | if string(b) != data { 183 | t.Fatalf("wrong data %v", b) 184 | } 185 | 186 | b, err = os.ReadFile(testFileName + ":ads.txt") 187 | if err != nil { 188 | t.Fatal(err) 189 | } 190 | if string(b) != altData { 191 | t.Fatalf("wrong data %v", b) 192 | } 193 | } 194 | 195 | func makeSparseFile() error { 196 | os.Remove(testFileName) 197 | f, err := os.Create(testFileName) 198 | if err != nil { 199 | return err 200 | } 201 | defer f.Close() 202 | 203 | err = windows.DeviceIoControl(windows.Handle(f.Fd()), windows.FSCTL_SET_SPARSE, nil, 0, nil, 0, nil, nil) 204 | if err != nil { 205 | return err 206 | } 207 | 208 | _, err = f.Write([]byte("testing 1 2 3\n")) 209 | if err != nil { 210 | return err 211 | } 212 | 213 | _, err = f.Seek(1000000, 0) 214 | if err != nil { 215 | return err 216 | } 217 | 218 | _, err = f.Write([]byte("more data later\n")) 219 | if err != nil { 220 | return err 221 | } 222 | 223 | return nil 224 | } 225 | 226 | func TestBackupSparseFile(t *testing.T) { 227 | err := makeSparseFile() 228 | if err != nil { 229 | t.Fatal(err) 230 | } 231 | 232 | f, err := os.Open(testFileName) 233 | if err != nil { 234 | t.Fatal(err) 235 | } 236 | defer f.Close() 237 | r := NewBackupFileReader(f, false) 238 | defer r.Close() 239 | 240 | br := NewBackupStreamReader(r) 241 | for { 242 | hdr, err := br.Next() 243 | if err == io.EOF { //nolint:errorlint 244 | break 245 | } 246 | if err != nil { 247 | t.Fatal(err) 248 | } 249 | 250 | t.Log(hdr) 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /internal/socket/socket.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package socket 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "net" 9 | "sync" 10 | "syscall" 11 | "unsafe" 12 | 13 | "github.com/Microsoft/go-winio/pkg/guid" 14 | "golang.org/x/sys/windows" 15 | ) 16 | 17 | //go:generate go run github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go socket.go 18 | 19 | //sys getsockname(s windows.Handle, name unsafe.Pointer, namelen *int32) (err error) [failretval==socketError] = ws2_32.getsockname 20 | //sys getpeername(s windows.Handle, name unsafe.Pointer, namelen *int32) (err error) [failretval==socketError] = ws2_32.getpeername 21 | //sys bind(s windows.Handle, name unsafe.Pointer, namelen int32) (err error) [failretval==socketError] = ws2_32.bind 22 | 23 | const socketError = uintptr(^uint32(0)) 24 | 25 | var ( 26 | // todo(helsaawy): create custom error types to store the desired vs actual size and addr family? 27 | 28 | ErrBufferSize = errors.New("buffer size") 29 | ErrAddrFamily = errors.New("address family") 30 | ErrInvalidPointer = errors.New("invalid pointer") 31 | ErrSocketClosed = fmt.Errorf("socket closed: %w", net.ErrClosed) 32 | ) 33 | 34 | // todo(helsaawy): replace these with generics, ie: GetSockName[S RawSockaddr](s windows.Handle) (S, error) 35 | 36 | // GetSockName writes the local address of socket s to the [RawSockaddr] rsa. 37 | // If rsa is not large enough, the [windows.WSAEFAULT] is returned. 38 | func GetSockName(s windows.Handle, rsa RawSockaddr) error { 39 | ptr, l, err := rsa.Sockaddr() 40 | if err != nil { 41 | return fmt.Errorf("could not retrieve socket pointer and size: %w", err) 42 | } 43 | 44 | // although getsockname returns WSAEFAULT if the buffer is too small, it does not set 45 | // &l to the correct size, so--apart from doubling the buffer repeatedly--there is no remedy 46 | return getsockname(s, ptr, &l) 47 | } 48 | 49 | // GetPeerName returns the remote address the socket is connected to. 50 | // 51 | // See [GetSockName] for more information. 52 | func GetPeerName(s windows.Handle, rsa RawSockaddr) error { 53 | ptr, l, err := rsa.Sockaddr() 54 | if err != nil { 55 | return fmt.Errorf("could not retrieve socket pointer and size: %w", err) 56 | } 57 | 58 | return getpeername(s, ptr, &l) 59 | } 60 | 61 | func Bind(s windows.Handle, rsa RawSockaddr) (err error) { 62 | ptr, l, err := rsa.Sockaddr() 63 | if err != nil { 64 | return fmt.Errorf("could not retrieve socket pointer and size: %w", err) 65 | } 66 | 67 | return bind(s, ptr, l) 68 | } 69 | 70 | // "golang.org/x/sys/windows".ConnectEx and .Bind only accept internal implementations of the 71 | // their sockaddr interface, so they cannot be used with HvsockAddr 72 | // Replicate functionality here from 73 | // https://cs.opensource.google/go/x/sys/+/master:windows/syscall_windows.go 74 | 75 | // The function pointers to `AcceptEx`, `ConnectEx` and `GetAcceptExSockaddrs` must be loaded at 76 | // runtime via a WSAIoctl call: 77 | // https://docs.microsoft.com/en-us/windows/win32/api/Mswsock/nc-mswsock-lpfn_connectex#remarks 78 | 79 | type runtimeFunc struct { 80 | id guid.GUID 81 | once sync.Once 82 | addr uintptr 83 | err error 84 | } 85 | 86 | func (f *runtimeFunc) Load() error { 87 | f.once.Do(func() { 88 | var s windows.Handle 89 | s, f.err = windows.Socket(windows.AF_INET, windows.SOCK_STREAM, windows.IPPROTO_TCP) 90 | if f.err != nil { 91 | return 92 | } 93 | defer windows.CloseHandle(s) //nolint:errcheck 94 | 95 | var n uint32 96 | f.err = windows.WSAIoctl(s, 97 | windows.SIO_GET_EXTENSION_FUNCTION_POINTER, 98 | (*byte)(unsafe.Pointer(&f.id)), 99 | uint32(unsafe.Sizeof(f.id)), 100 | (*byte)(unsafe.Pointer(&f.addr)), 101 | uint32(unsafe.Sizeof(f.addr)), 102 | &n, 103 | nil, // overlapped 104 | 0, // completionRoutine 105 | ) 106 | }) 107 | return f.err 108 | } 109 | 110 | var ( 111 | // todo: add `AcceptEx` and `GetAcceptExSockaddrs` 112 | WSAID_CONNECTEX = guid.GUID{ //revive:disable-line:var-naming ALL_CAPS 113 | Data1: 0x25a207b9, 114 | Data2: 0xddf3, 115 | Data3: 0x4660, 116 | Data4: [8]byte{0x8e, 0xe9, 0x76, 0xe5, 0x8c, 0x74, 0x06, 0x3e}, 117 | } 118 | 119 | connectExFunc = runtimeFunc{id: WSAID_CONNECTEX} 120 | ) 121 | 122 | func ConnectEx( 123 | fd windows.Handle, 124 | rsa RawSockaddr, 125 | sendBuf *byte, 126 | sendDataLen uint32, 127 | bytesSent *uint32, 128 | overlapped *windows.Overlapped, 129 | ) error { 130 | if err := connectExFunc.Load(); err != nil { 131 | return fmt.Errorf("failed to load ConnectEx function pointer: %w", err) 132 | } 133 | ptr, n, err := rsa.Sockaddr() 134 | if err != nil { 135 | return err 136 | } 137 | return connectEx(fd, ptr, n, sendBuf, sendDataLen, bytesSent, overlapped) 138 | } 139 | 140 | // BOOL LpfnConnectex( 141 | // [in] SOCKET s, 142 | // [in] const sockaddr *name, 143 | // [in] int namelen, 144 | // [in, optional] PVOID lpSendBuffer, 145 | // [in] DWORD dwSendDataLength, 146 | // [out] LPDWORD lpdwBytesSent, 147 | // [in] LPOVERLAPPED lpOverlapped 148 | // ) 149 | 150 | func connectEx( 151 | s windows.Handle, 152 | name unsafe.Pointer, 153 | namelen int32, 154 | sendBuf *byte, 155 | sendDataLen uint32, 156 | bytesSent *uint32, 157 | overlapped *windows.Overlapped, 158 | ) (err error) { 159 | r1, _, e1 := syscall.SyscallN(connectExFunc.addr, 160 | uintptr(s), 161 | uintptr(name), 162 | uintptr(namelen), 163 | uintptr(unsafe.Pointer(sendBuf)), 164 | uintptr(sendDataLen), 165 | uintptr(unsafe.Pointer(bytesSent)), 166 | uintptr(unsafe.Pointer(overlapped)), 167 | ) 168 | 169 | if r1 == 0 { 170 | if e1 != 0 { 171 | err = error(e1) 172 | } else { 173 | err = syscall.EINVAL 174 | } 175 | } 176 | return err 177 | } 178 | -------------------------------------------------------------------------------- /pkg/security/grantvmgroupaccess.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package security 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | "unsafe" 9 | 10 | "golang.org/x/sys/windows" 11 | ) 12 | 13 | type ( 14 | accessMask uint32 15 | accessMode uint32 16 | desiredAccess uint32 17 | inheritMode uint32 18 | objectType uint32 19 | shareMode uint32 20 | securityInformation uint32 21 | trusteeForm uint32 22 | trusteeType uint32 23 | 24 | explicitAccess struct { 25 | accessPermissions accessMask 26 | accessMode accessMode 27 | inheritance inheritMode 28 | trustee trustee 29 | } 30 | 31 | trustee struct { 32 | multipleTrustee *trustee 33 | multipleTrusteeOperation int32 34 | trusteeForm trusteeForm 35 | trusteeType trusteeType 36 | name uintptr 37 | } 38 | ) 39 | 40 | const ( 41 | accessMaskDesiredPermission accessMask = 1 << 31 // GENERIC_READ 42 | 43 | accessModeGrant accessMode = 1 44 | 45 | desiredAccessReadControl desiredAccess = 0x20000 46 | desiredAccessWriteDac desiredAccess = 0x40000 47 | 48 | //cspell:disable-next-line 49 | gvmga = "GrantVmGroupAccess:" 50 | 51 | inheritModeNoInheritance inheritMode = 0x0 52 | inheritModeSubContainersAndObjectsInherit inheritMode = 0x3 53 | 54 | objectTypeFileObject objectType = 0x1 55 | 56 | securityInformationDACL securityInformation = 0x4 57 | 58 | shareModeRead shareMode = 0x1 59 | shareModeWrite shareMode = 0x2 60 | 61 | sidVMGroup = "S-1-5-83-0" 62 | 63 | trusteeFormIsSID trusteeForm = 0 64 | 65 | trusteeTypeWellKnownGroup trusteeType = 5 66 | ) 67 | 68 | // GrantVMGroupAccess sets the DACL for a specified file or directory to 69 | // include Grant ACE entries for the VM Group SID. This is a golang re- 70 | // implementation of the same function in vmcompute, just not exported in 71 | // RS5. Which kind of sucks. Sucks a lot :/ 72 | // 73 | //revive:disable-next-line:var-naming VM, not Vm 74 | func GrantVmGroupAccess(name string) error { 75 | // Stat (to determine if `name` is a directory). 76 | s, err := os.Stat(name) 77 | if err != nil { 78 | return fmt.Errorf("%s os.Stat %s: %w", gvmga, name, err) 79 | } 80 | 81 | // Get a handle to the file/directory. Must defer Close on success. 82 | fd, err := createFile(name, s.IsDir()) 83 | if err != nil { 84 | return err // Already wrapped 85 | } 86 | defer windows.CloseHandle(fd) //nolint:errcheck 87 | 88 | // Get the current DACL and Security Descriptor. Must defer LocalFree on success. 89 | ot := objectTypeFileObject 90 | si := securityInformationDACL 91 | sd := uintptr(0) 92 | origDACL := uintptr(0) 93 | if err := getSecurityInfo(fd, uint32(ot), uint32(si), nil, nil, &origDACL, nil, &sd); err != nil { 94 | return fmt.Errorf("%s GetSecurityInfo %s: %w", gvmga, name, err) 95 | } 96 | defer windows.LocalFree(windows.Handle(sd)) //nolint:errcheck 97 | 98 | // Generate a new DACL which is the current DACL with the required ACEs added. 99 | // Must defer LocalFree on success. 100 | newDACL, err := generateDACLWithAcesAdded(name, s.IsDir(), origDACL) 101 | if err != nil { 102 | return err // Already wrapped 103 | } 104 | defer windows.LocalFree(windows.Handle(newDACL)) //nolint:errcheck 105 | 106 | // And finally use SetSecurityInfo to apply the updated DACL. 107 | if err := setSecurityInfo(fd, uint32(ot), uint32(si), uintptr(0), uintptr(0), newDACL, uintptr(0)); err != nil { 108 | return fmt.Errorf("%s SetSecurityInfo %s: %w", gvmga, name, err) 109 | } 110 | 111 | return nil 112 | } 113 | 114 | // createFile is a helper function to call [Nt]CreateFile to get a handle to 115 | // the file or directory. 116 | func createFile(name string, isDir bool) (windows.Handle, error) { 117 | namep, err := windows.UTF16FromString(name) 118 | if err != nil { 119 | return windows.InvalidHandle, fmt.Errorf("could not convernt name to UTF-16: %w", err) 120 | } 121 | da := uint32(desiredAccessReadControl | desiredAccessWriteDac) 122 | sm := uint32(shareModeRead | shareModeWrite) 123 | fa := uint32(windows.FILE_ATTRIBUTE_NORMAL) 124 | if isDir { 125 | fa |= windows.FILE_FLAG_BACKUP_SEMANTICS 126 | } 127 | fd, err := windows.CreateFile(&namep[0], da, sm, nil, windows.OPEN_EXISTING, fa, 0) 128 | if err != nil { 129 | return windows.InvalidHandle, fmt.Errorf("%s windows.CreateFile %s: %w", gvmga, name, err) 130 | } 131 | return fd, nil 132 | } 133 | 134 | // generateDACLWithAcesAdded generates a new DACL with the two needed ACEs added. 135 | // The caller is responsible for LocalFree of the returned DACL on success. 136 | func generateDACLWithAcesAdded(name string, isDir bool, origDACL uintptr) (uintptr, error) { 137 | // Generate pointers to the SIDs based on the string SIDs 138 | sid, err := windows.StringToSid(sidVMGroup) 139 | if err != nil { 140 | return 0, fmt.Errorf("%s windows.StringToSid %s %s: %w", gvmga, name, sidVMGroup, err) 141 | } 142 | 143 | inheritance := inheritModeNoInheritance 144 | if isDir { 145 | inheritance = inheritModeSubContainersAndObjectsInherit 146 | } 147 | 148 | eaArray := []explicitAccess{ 149 | { 150 | accessPermissions: accessMaskDesiredPermission, 151 | accessMode: accessModeGrant, 152 | inheritance: inheritance, 153 | trustee: trustee{ 154 | trusteeForm: trusteeFormIsSID, 155 | trusteeType: trusteeTypeWellKnownGroup, 156 | name: uintptr(unsafe.Pointer(sid)), 157 | }, 158 | }, 159 | } 160 | 161 | modifiedDACL := uintptr(0) 162 | if err := setEntriesInAcl(uintptr(uint32(1)), uintptr(unsafe.Pointer(&eaArray[0])), origDACL, &modifiedDACL); err != nil { 163 | return 0, fmt.Errorf("%s SetEntriesInAcl %s: %w", gvmga, name, err) 164 | } 165 | 166 | return modifiedDACL, nil 167 | } 168 | -------------------------------------------------------------------------------- /vhd/zvhd_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | // Code generated by 'go generate' using "github.com/Microsoft/go-winio/tools/mkwinsyscall"; DO NOT EDIT. 4 | 5 | package vhd 6 | 7 | import ( 8 | "syscall" 9 | "unsafe" 10 | 11 | "golang.org/x/sys/windows" 12 | ) 13 | 14 | var _ unsafe.Pointer 15 | 16 | // Do the interface allocations only once for common 17 | // Errno values. 18 | const ( 19 | errnoERROR_IO_PENDING = 997 20 | ) 21 | 22 | var ( 23 | errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING) 24 | errERROR_EINVAL error = syscall.EINVAL 25 | ) 26 | 27 | // errnoErr returns common boxed Errno values, to prevent 28 | // allocations at runtime. 29 | func errnoErr(e syscall.Errno) error { 30 | switch e { 31 | case 0: 32 | return errERROR_EINVAL 33 | case errnoERROR_IO_PENDING: 34 | return errERROR_IO_PENDING 35 | } 36 | return e 37 | } 38 | 39 | var ( 40 | modvirtdisk = windows.NewLazySystemDLL("virtdisk.dll") 41 | 42 | procAttachVirtualDisk = modvirtdisk.NewProc("AttachVirtualDisk") 43 | procCreateVirtualDisk = modvirtdisk.NewProc("CreateVirtualDisk") 44 | procDetachVirtualDisk = modvirtdisk.NewProc("DetachVirtualDisk") 45 | procGetVirtualDiskInformation = modvirtdisk.NewProc("GetVirtualDiskInformation") 46 | procGetVirtualDiskPhysicalPath = modvirtdisk.NewProc("GetVirtualDiskPhysicalPath") 47 | procOpenVirtualDisk = modvirtdisk.NewProc("OpenVirtualDisk") 48 | procSetVirtualDiskInformation = modvirtdisk.NewProc("SetVirtualDiskInformation") 49 | ) 50 | 51 | func attachVirtualDisk(handle syscall.Handle, securityDescriptor *uintptr, attachVirtualDiskFlag uint32, providerSpecificFlags uint32, parameters *AttachVirtualDiskParameters, overlapped *syscall.Overlapped) (win32err error) { 52 | r0, _, _ := syscall.SyscallN(procAttachVirtualDisk.Addr(), uintptr(handle), uintptr(unsafe.Pointer(securityDescriptor)), uintptr(attachVirtualDiskFlag), uintptr(providerSpecificFlags), uintptr(unsafe.Pointer(parameters)), uintptr(unsafe.Pointer(overlapped))) 53 | if r0 != 0 { 54 | win32err = syscall.Errno(r0) 55 | } 56 | return 57 | } 58 | 59 | func createVirtualDisk(virtualStorageType *VirtualStorageType, path string, virtualDiskAccessMask uint32, securityDescriptor *uintptr, createVirtualDiskFlags uint32, providerSpecificFlags uint32, parameters *CreateVirtualDiskParameters, overlapped *syscall.Overlapped, handle *syscall.Handle) (win32err error) { 60 | var _p0 *uint16 61 | _p0, win32err = syscall.UTF16PtrFromString(path) 62 | if win32err != nil { 63 | return 64 | } 65 | return _createVirtualDisk(virtualStorageType, _p0, virtualDiskAccessMask, securityDescriptor, createVirtualDiskFlags, providerSpecificFlags, parameters, overlapped, handle) 66 | } 67 | 68 | func _createVirtualDisk(virtualStorageType *VirtualStorageType, path *uint16, virtualDiskAccessMask uint32, securityDescriptor *uintptr, createVirtualDiskFlags uint32, providerSpecificFlags uint32, parameters *CreateVirtualDiskParameters, overlapped *syscall.Overlapped, handle *syscall.Handle) (win32err error) { 69 | r0, _, _ := syscall.SyscallN(procCreateVirtualDisk.Addr(), uintptr(unsafe.Pointer(virtualStorageType)), uintptr(unsafe.Pointer(path)), uintptr(virtualDiskAccessMask), uintptr(unsafe.Pointer(securityDescriptor)), uintptr(createVirtualDiskFlags), uintptr(providerSpecificFlags), uintptr(unsafe.Pointer(parameters)), uintptr(unsafe.Pointer(overlapped)), uintptr(unsafe.Pointer(handle))) 70 | if r0 != 0 { 71 | win32err = syscall.Errno(r0) 72 | } 73 | return 74 | } 75 | 76 | func detachVirtualDisk(handle syscall.Handle, detachVirtualDiskFlags uint32, providerSpecificFlags uint32) (win32err error) { 77 | r0, _, _ := syscall.SyscallN(procDetachVirtualDisk.Addr(), uintptr(handle), uintptr(detachVirtualDiskFlags), uintptr(providerSpecificFlags)) 78 | if r0 != 0 { 79 | win32err = syscall.Errno(r0) 80 | } 81 | return 82 | } 83 | 84 | func getVirtualDiskInformation(handle syscall.Handle, bufferSize *uint32, info *virtualDiskInfo, sizeUsed *uint32) (win32err error) { 85 | r0, _, _ := syscall.SyscallN(procGetVirtualDiskInformation.Addr(), uintptr(handle), uintptr(unsafe.Pointer(bufferSize)), uintptr(unsafe.Pointer(info)), uintptr(unsafe.Pointer(sizeUsed))) 86 | if r0 != 0 { 87 | win32err = syscall.Errno(r0) 88 | } 89 | return 90 | } 91 | 92 | func getVirtualDiskPhysicalPath(handle syscall.Handle, diskPathSizeInBytes *uint32, buffer *uint16) (win32err error) { 93 | r0, _, _ := syscall.SyscallN(procGetVirtualDiskPhysicalPath.Addr(), uintptr(handle), uintptr(unsafe.Pointer(diskPathSizeInBytes)), uintptr(unsafe.Pointer(buffer))) 94 | if r0 != 0 { 95 | win32err = syscall.Errno(r0) 96 | } 97 | return 98 | } 99 | 100 | func openVirtualDisk(virtualStorageType *VirtualStorageType, path string, virtualDiskAccessMask uint32, openVirtualDiskFlags uint32, parameters *openVirtualDiskParameters, handle *syscall.Handle) (win32err error) { 101 | var _p0 *uint16 102 | _p0, win32err = syscall.UTF16PtrFromString(path) 103 | if win32err != nil { 104 | return 105 | } 106 | return _openVirtualDisk(virtualStorageType, _p0, virtualDiskAccessMask, openVirtualDiskFlags, parameters, handle) 107 | } 108 | 109 | func _openVirtualDisk(virtualStorageType *VirtualStorageType, path *uint16, virtualDiskAccessMask uint32, openVirtualDiskFlags uint32, parameters *openVirtualDiskParameters, handle *syscall.Handle) (win32err error) { 110 | r0, _, _ := syscall.SyscallN(procOpenVirtualDisk.Addr(), uintptr(unsafe.Pointer(virtualStorageType)), uintptr(unsafe.Pointer(path)), uintptr(virtualDiskAccessMask), uintptr(openVirtualDiskFlags), uintptr(unsafe.Pointer(parameters)), uintptr(unsafe.Pointer(handle))) 111 | if r0 != 0 { 112 | win32err = syscall.Errno(r0) 113 | } 114 | return 115 | } 116 | 117 | func setVirtualDiskInformation(handle syscall.Handle, info *virtualDiskInfo) (win32err error) { 118 | r0, _, _ := syscall.SyscallN(procSetVirtualDiskInformation.Addr(), uintptr(handle), uintptr(unsafe.Pointer(info))) 119 | if r0 != 0 { 120 | win32err = syscall.Errno(r0) 121 | } 122 | return 123 | } 124 | -------------------------------------------------------------------------------- /fileinfo_test.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package winio 4 | 5 | import ( 6 | "os" 7 | "testing" 8 | "unsafe" 9 | 10 | "golang.org/x/sys/windows" 11 | ) 12 | 13 | // Checks if current matches expected. Note that AllocationSize is filesystem-specific, 14 | // so we check that the current.AllocationSize is >= expected.AllocationSize. 15 | // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/5afa7f66-619c-48f3-955f-68c4ece704ae 16 | func checkFileStandardInfo(t *testing.T, current, expected *FileStandardInfo) { 17 | t.Helper() 18 | 19 | if current.AllocationSize < expected.AllocationSize { 20 | t.Fatalf("FileStandardInfo unexpectedly had AllocationSize %d, expecting >=%d", current.AllocationSize, expected.AllocationSize) 21 | } 22 | 23 | if current.EndOfFile != expected.EndOfFile { 24 | t.Fatalf("FileStandardInfo unexpectedly had EndOfFile %d, expecting %d", current.EndOfFile, expected.EndOfFile) 25 | } 26 | 27 | if current.NumberOfLinks != expected.NumberOfLinks { 28 | t.Fatalf("FileStandardInfo unexpectedly had NumberOfLinks %d, expecting %d", current.NumberOfLinks, expected.NumberOfLinks) 29 | } 30 | 31 | if current.DeletePending != expected.DeletePending { 32 | if current.DeletePending { 33 | t.Fatalf("FileStandardInfo unexpectedly DeletePending") 34 | } else { 35 | t.Fatalf("FileStandardInfo unexpectedly not DeletePending") 36 | } 37 | } 38 | 39 | if current.Directory != expected.Directory { 40 | if current.Directory { 41 | t.Fatalf("FileStandardInfo unexpectedly Directory") 42 | } else { 43 | t.Fatalf("FileStandardInfo unexpectedly not Directory") 44 | } 45 | } 46 | } 47 | 48 | func TestGetFileStandardInfo_File(t *testing.T) { 49 | f, err := os.CreateTemp("", "tst") 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | defer f.Close() 54 | defer os.Remove(f.Name()) 55 | 56 | expectedFileInfo := &FileStandardInfo{ 57 | AllocationSize: 0, 58 | EndOfFile: 0, 59 | NumberOfLinks: 1, 60 | DeletePending: false, 61 | Directory: false, 62 | } 63 | 64 | info, err := GetFileStandardInfo(f) 65 | if err != nil { 66 | t.Fatal(err) 67 | } 68 | checkFileStandardInfo(t, info, expectedFileInfo) 69 | 70 | bytesWritten, err := f.Write([]byte("0123456789")) 71 | if err != nil { 72 | t.Fatal(err) 73 | } 74 | 75 | expectedFileInfo.EndOfFile = int64(bytesWritten) 76 | expectedFileInfo.AllocationSize = int64(bytesWritten) 77 | 78 | info, err = GetFileStandardInfo(f) 79 | if err != nil { 80 | t.Fatal(err) 81 | } 82 | checkFileStandardInfo(t, info, expectedFileInfo) 83 | 84 | linkName := f.Name() + ".link" 85 | 86 | if err = os.Link(f.Name(), linkName); err != nil { 87 | t.Fatal(err) 88 | } 89 | defer os.Remove(linkName) 90 | 91 | expectedFileInfo.NumberOfLinks = 2 92 | 93 | info, err = GetFileStandardInfo(f) 94 | if err != nil { 95 | t.Fatal(err) 96 | } 97 | checkFileStandardInfo(t, info, expectedFileInfo) 98 | 99 | os.Remove(linkName) 100 | 101 | expectedFileInfo.NumberOfLinks = 1 102 | 103 | info, err = GetFileStandardInfo(f) 104 | if err != nil { 105 | t.Fatal(err) 106 | } 107 | checkFileStandardInfo(t, info, expectedFileInfo) 108 | } 109 | 110 | func TestGetFileStandardInfo_Directory(t *testing.T) { 111 | tempDir := t.TempDir() 112 | // os.Open returns the Search Handle, not the Directory Handle 113 | // See https://github.com/golang/go/issues/13738 114 | f, err := OpenForBackup(tempDir, windows.GENERIC_READ, 0, windows.OPEN_EXISTING) 115 | if err != nil { 116 | t.Fatal(err) 117 | } 118 | defer f.Close() 119 | 120 | expectedFileInfo := &FileStandardInfo{ 121 | AllocationSize: 0, 122 | EndOfFile: 0, 123 | NumberOfLinks: 1, 124 | DeletePending: false, 125 | Directory: true, 126 | } 127 | 128 | info, err := GetFileStandardInfo(f) 129 | if err != nil { 130 | t.Fatal(err) 131 | } 132 | checkFileStandardInfo(t, info, expectedFileInfo) 133 | } 134 | 135 | // TestFileInfoStructAlignment checks that the alignment of Go fileinfo structs 136 | // match what is expected by the Windows API. 137 | func TestFileInfoStructAlignment(t *testing.T) { 138 | //nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API. 139 | const ( 140 | // The alignment of various types, as named in the Windows APIs. When 141 | // deciding on an expectedAlignment for a struct's test case, use the 142 | // type of the largest field in the struct as written in the Windows 143 | // docs. This is intended to help reviewers by allowing them to first 144 | // check that a new align* const is correct, then independently check 145 | // that the test case is correct, rather than all at once. 146 | alignLARGE_INTEGER = unsafe.Alignof(uint64(0)) 147 | alignULONGLONG = unsafe.Alignof(uint64(0)) 148 | ) 149 | tests := []struct { 150 | name string 151 | actualAlign uintptr 152 | actualSize uintptr 153 | expectedAlignment uintptr 154 | }{ 155 | { 156 | // alignedFileBasicInfo is passed to the Windows API rather than FileBasicInfo. 157 | "alignedFileBasicInfo", unsafe.Alignof(alignedFileBasicInfo{}), unsafe.Sizeof(alignedFileBasicInfo{}), 158 | // https://learn.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-file_basic_info 159 | alignLARGE_INTEGER, 160 | }, 161 | { 162 | "FileStandardInfo", unsafe.Alignof(FileStandardInfo{}), unsafe.Sizeof(FileStandardInfo{}), 163 | // https://learn.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-file_standard_info 164 | alignLARGE_INTEGER, 165 | }, 166 | { 167 | "FileIDInfo", unsafe.Alignof(FileIDInfo{}), unsafe.Sizeof(FileIDInfo{}), 168 | // https://learn.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-file_id_info 169 | alignULONGLONG, 170 | }, 171 | } 172 | for _, tt := range tests { 173 | t.Run(tt.name, func(t *testing.T) { 174 | if tt.actualAlign != tt.expectedAlignment { 175 | t.Errorf("alignment mismatch: actual %d, expected %d", tt.actualAlign, tt.expectedAlignment) 176 | } 177 | if r := tt.actualSize % tt.expectedAlignment; r != 0 { 178 | t.Errorf( 179 | "size is not a multiple of alignment: size %% alignment (%d %% %d) is %d, expected 0", 180 | tt.actualSize, tt.expectedAlignment, r) 181 | } 182 | }) 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /privilege.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package winio 4 | 5 | import ( 6 | "bytes" 7 | "encoding/binary" 8 | "fmt" 9 | "runtime" 10 | "sync" 11 | "unicode/utf16" 12 | 13 | "golang.org/x/sys/windows" 14 | ) 15 | 16 | //sys adjustTokenPrivileges(token windows.Token, releaseAll bool, input *byte, outputSize uint32, output *byte, requiredSize *uint32) (success bool, err error) [true] = advapi32.AdjustTokenPrivileges 17 | //sys impersonateSelf(level uint32) (err error) = advapi32.ImpersonateSelf 18 | //sys revertToSelf() (err error) = advapi32.RevertToSelf 19 | //sys openThreadToken(thread windows.Handle, accessMask uint32, openAsSelf bool, token *windows.Token) (err error) = advapi32.OpenThreadToken 20 | //sys getCurrentThread() (h windows.Handle) = GetCurrentThread 21 | //sys lookupPrivilegeValue(systemName string, name string, luid *uint64) (err error) = advapi32.LookupPrivilegeValueW 22 | //sys lookupPrivilegeName(systemName string, luid *uint64, buffer *uint16, size *uint32) (err error) = advapi32.LookupPrivilegeNameW 23 | //sys lookupPrivilegeDisplayName(systemName string, name *uint16, buffer *uint16, size *uint32, languageId *uint32) (err error) = advapi32.LookupPrivilegeDisplayNameW 24 | 25 | const ( 26 | //revive:disable-next-line:var-naming ALL_CAPS 27 | SE_PRIVILEGE_ENABLED = windows.SE_PRIVILEGE_ENABLED 28 | 29 | //revive:disable-next-line:var-naming ALL_CAPS 30 | ERROR_NOT_ALL_ASSIGNED windows.Errno = windows.ERROR_NOT_ALL_ASSIGNED 31 | 32 | SeBackupPrivilege = "SeBackupPrivilege" 33 | SeRestorePrivilege = "SeRestorePrivilege" 34 | SeSecurityPrivilege = "SeSecurityPrivilege" 35 | ) 36 | 37 | var ( 38 | privNames = make(map[string]uint64) 39 | privNameMutex sync.Mutex 40 | ) 41 | 42 | // PrivilegeError represents an error enabling privileges. 43 | type PrivilegeError struct { 44 | privileges []uint64 45 | } 46 | 47 | func (e *PrivilegeError) Error() string { 48 | s := "Could not enable privilege " 49 | if len(e.privileges) > 1 { 50 | s = "Could not enable privileges " 51 | } 52 | for i, p := range e.privileges { 53 | if i != 0 { 54 | s += ", " 55 | } 56 | s += `"` 57 | s += getPrivilegeName(p) 58 | s += `"` 59 | } 60 | return s 61 | } 62 | 63 | // RunWithPrivilege enables a single privilege for a function call. 64 | func RunWithPrivilege(name string, fn func() error) error { 65 | return RunWithPrivileges([]string{name}, fn) 66 | } 67 | 68 | // RunWithPrivileges enables privileges for a function call. 69 | func RunWithPrivileges(names []string, fn func() error) error { 70 | privileges, err := mapPrivileges(names) 71 | if err != nil { 72 | return err 73 | } 74 | runtime.LockOSThread() 75 | defer runtime.UnlockOSThread() 76 | token, err := newThreadToken() 77 | if err != nil { 78 | return err 79 | } 80 | defer releaseThreadToken(token) 81 | err = adjustPrivileges(token, privileges, SE_PRIVILEGE_ENABLED) 82 | if err != nil { 83 | return err 84 | } 85 | return fn() 86 | } 87 | 88 | func mapPrivileges(names []string) ([]uint64, error) { 89 | privileges := make([]uint64, 0, len(names)) 90 | privNameMutex.Lock() 91 | defer privNameMutex.Unlock() 92 | for _, name := range names { 93 | p, ok := privNames[name] 94 | if !ok { 95 | err := lookupPrivilegeValue("", name, &p) 96 | if err != nil { 97 | return nil, err 98 | } 99 | privNames[name] = p 100 | } 101 | privileges = append(privileges, p) 102 | } 103 | return privileges, nil 104 | } 105 | 106 | // EnableProcessPrivileges enables privileges globally for the process. 107 | func EnableProcessPrivileges(names []string) error { 108 | return enableDisableProcessPrivilege(names, SE_PRIVILEGE_ENABLED) 109 | } 110 | 111 | // DisableProcessPrivileges disables privileges globally for the process. 112 | func DisableProcessPrivileges(names []string) error { 113 | return enableDisableProcessPrivilege(names, 0) 114 | } 115 | 116 | func enableDisableProcessPrivilege(names []string, action uint32) error { 117 | privileges, err := mapPrivileges(names) 118 | if err != nil { 119 | return err 120 | } 121 | 122 | p := windows.CurrentProcess() 123 | var token windows.Token 124 | err = windows.OpenProcessToken(p, windows.TOKEN_ADJUST_PRIVILEGES|windows.TOKEN_QUERY, &token) 125 | if err != nil { 126 | return err 127 | } 128 | 129 | defer token.Close() 130 | return adjustPrivileges(token, privileges, action) 131 | } 132 | 133 | func adjustPrivileges(token windows.Token, privileges []uint64, action uint32) error { 134 | var b bytes.Buffer 135 | _ = binary.Write(&b, binary.LittleEndian, uint32(len(privileges))) 136 | for _, p := range privileges { 137 | _ = binary.Write(&b, binary.LittleEndian, p) 138 | _ = binary.Write(&b, binary.LittleEndian, action) 139 | } 140 | prevState := make([]byte, b.Len()) 141 | reqSize := uint32(0) 142 | success, err := adjustTokenPrivileges(token, false, &b.Bytes()[0], uint32(len(prevState)), &prevState[0], &reqSize) 143 | if !success { 144 | return err 145 | } 146 | if err == ERROR_NOT_ALL_ASSIGNED { //nolint:errorlint // err is Errno 147 | return &PrivilegeError{privileges} 148 | } 149 | return nil 150 | } 151 | 152 | func getPrivilegeName(luid uint64) string { 153 | var nameBuffer [256]uint16 154 | bufSize := uint32(len(nameBuffer)) 155 | err := lookupPrivilegeName("", &luid, &nameBuffer[0], &bufSize) 156 | if err != nil { 157 | return fmt.Sprintf("", luid) 158 | } 159 | 160 | var displayNameBuffer [256]uint16 161 | displayBufSize := uint32(len(displayNameBuffer)) 162 | var langID uint32 163 | err = lookupPrivilegeDisplayName("", &nameBuffer[0], &displayNameBuffer[0], &displayBufSize, &langID) 164 | if err != nil { 165 | return fmt.Sprintf("", string(utf16.Decode(nameBuffer[:bufSize]))) 166 | } 167 | 168 | return string(utf16.Decode(displayNameBuffer[:displayBufSize])) 169 | } 170 | 171 | func newThreadToken() (windows.Token, error) { 172 | err := impersonateSelf(windows.SecurityImpersonation) 173 | if err != nil { 174 | return 0, err 175 | } 176 | 177 | var token windows.Token 178 | err = openThreadToken(getCurrentThread(), windows.TOKEN_ADJUST_PRIVILEGES|windows.TOKEN_QUERY, false, &token) 179 | if err != nil { 180 | rerr := revertToSelf() 181 | if rerr != nil { 182 | panic(rerr) 183 | } 184 | return 0, err 185 | } 186 | return token, nil 187 | } 188 | 189 | func releaseThreadToken(h windows.Token) { 190 | err := revertToSelf() 191 | if err != nil { 192 | panic(err) 193 | } 194 | h.Close() 195 | } 196 | -------------------------------------------------------------------------------- /pkg/etw/eventmetadata.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package etw 4 | 5 | import ( 6 | "bytes" 7 | "encoding/binary" 8 | ) 9 | 10 | // inType indicates the type of data contained in the ETW event. 11 | type inType byte 12 | 13 | // Various inType definitions for TraceLogging. These must match the definitions 14 | // found in TraceLoggingProvider.h in the Windows SDK. 15 | // 16 | //nolint:deadcode,varcheck // keep unused constants for potential future use 17 | const ( 18 | inTypeNull inType = iota 19 | inTypeUnicodeString 20 | inTypeANSIString 21 | inTypeInt8 22 | inTypeUint8 23 | inTypeInt16 24 | inTypeUint16 25 | inTypeInt32 26 | inTypeUint32 27 | inTypeInt64 28 | inTypeUint64 29 | inTypeFloat 30 | inTypeDouble 31 | inTypeBool32 32 | inTypeBinary 33 | inTypeGUID 34 | inTypePointerUnsupported 35 | inTypeFileTime 36 | inTypeSystemTime 37 | inTypeSID 38 | inTypeHexInt32 39 | inTypeHexInt64 40 | inTypeCountedString 41 | inTypeCountedANSIString 42 | inTypeStruct 43 | inTypeCountedBinary 44 | inTypeCountedArray inType = 32 45 | inTypeArray inType = 64 46 | ) 47 | 48 | // outType specifies a hint to the event decoder for how the value should be 49 | // formatted. 50 | type outType byte 51 | 52 | // Various outType definitions for TraceLogging. These must match the 53 | // definitions found in TraceLoggingProvider.h in the Windows SDK. 54 | // 55 | //nolint:deadcode,varcheck // keep unused constants for potential future use 56 | const ( 57 | // outTypeDefault indicates that the default formatting for the inType will 58 | // be used by the event decoder. 59 | outTypeDefault outType = iota 60 | outTypeNoPrint 61 | outTypeString 62 | outTypeBoolean 63 | outTypeHex 64 | outTypePID 65 | outTypeTID 66 | outTypePort 67 | outTypeIPv4 68 | outTypeIPv6 69 | outTypeSocketAddress 70 | outTypeXML 71 | outTypeJSON 72 | outTypeWin32Error 73 | outTypeNTStatus 74 | outTypeHResult 75 | outTypeFileTime 76 | outTypeSigned 77 | outTypeUnsigned 78 | outTypeUTF8 outType = 35 79 | outTypePKCS7WithTypeInfo outType = 36 80 | outTypeCodePointer outType = 37 81 | outTypeDateTimeUTC outType = 38 82 | ) 83 | 84 | // eventMetadata maintains a buffer which builds up the metadata for an ETW 85 | // event. It needs to be paired with EventData which describes the event. 86 | type eventMetadata struct { 87 | buffer bytes.Buffer 88 | } 89 | 90 | // toBytes returns the raw binary data containing the event metadata. Before being 91 | // returned, the current size of the buffer is written to the start of the 92 | // buffer. The returned value is not copied from the internal buffer, so it can 93 | // be mutated by the eventMetadata object after it is returned. 94 | func (em *eventMetadata) toBytes() []byte { 95 | // Finalize the event metadata buffer by filling in the buffer length at the 96 | // beginning. 97 | binary.LittleEndian.PutUint16(em.buffer.Bytes(), uint16(em.buffer.Len())) 98 | return em.buffer.Bytes() 99 | } 100 | 101 | // writeEventHeader writes the metadata for the start of an event to the buffer. 102 | // This specifies the event name and tags. 103 | func (em *eventMetadata) writeEventHeader(name string, tags uint32) { 104 | _ = binary.Write(&em.buffer, binary.LittleEndian, uint16(0)) // Length placeholder 105 | em.writeTags(tags) 106 | em.buffer.WriteString(name) 107 | em.buffer.WriteByte(0) // Null terminator for name 108 | } 109 | 110 | func (em *eventMetadata) writeFieldInner(name string, inType inType, outType outType, tags uint32, arrSize uint16) { 111 | em.buffer.WriteString(name) 112 | em.buffer.WriteByte(0) // Null terminator for name 113 | 114 | if outType == outTypeDefault && tags == 0 { 115 | em.buffer.WriteByte(byte(inType)) 116 | } else { 117 | em.buffer.WriteByte(byte(inType | 128)) 118 | if tags == 0 { 119 | em.buffer.WriteByte(byte(outType)) 120 | } else { 121 | em.buffer.WriteByte(byte(outType | 128)) 122 | em.writeTags(tags) 123 | } 124 | } 125 | 126 | if arrSize != 0 { 127 | _ = binary.Write(&em.buffer, binary.LittleEndian, arrSize) 128 | } 129 | } 130 | 131 | // writeTags writes out the tags value to the event metadata. Tags is a 28-bit 132 | // value, interpreted as bit flags, which are only relevant to the event 133 | // consumer. The event consumer may choose to attribute special meaning to tags 134 | // (e.g. 0x4 could mean the field contains PII). Tags are written as a series of 135 | // bytes, each containing 7 bits of tag value, with the high bit set if there is 136 | // more tag data in the following byte. This allows for a more compact 137 | // representation when not all of the tag bits are needed. 138 | func (em *eventMetadata) writeTags(tags uint32) { 139 | // Only use the top 28 bits of the tags value. 140 | tags &= 0xfffffff 141 | 142 | for { 143 | // Tags are written with the most significant bits (e.g. 21-27) first. 144 | val := tags >> 21 145 | 146 | if tags&0x1fffff == 0 { 147 | // If there is no more data to write after this, write this value 148 | // without the high bit set, and return. 149 | em.buffer.WriteByte(byte(val & 0x7f)) 150 | return 151 | } 152 | 153 | em.buffer.WriteByte(byte(val | 0x80)) 154 | 155 | tags <<= 7 156 | } 157 | } 158 | 159 | // writeField writes the metadata for a simple field to the buffer. 160 | // 161 | //nolint:unparam // tags is currently always 0, may change in the future 162 | func (em *eventMetadata) writeField(name string, inType inType, outType outType, tags uint32) { 163 | em.writeFieldInner(name, inType, outType, tags, 0) 164 | } 165 | 166 | // writeArray writes the metadata for an array field to the buffer. The number 167 | // of elements in the array must be written as a uint16 in the event data, 168 | // immediately preceding the event data. 169 | // 170 | //nolint:unparam // tags is currently always 0, may change in the future 171 | func (em *eventMetadata) writeArray(name string, inType inType, outType outType, tags uint32) { 172 | em.writeFieldInner(name, inType|inTypeArray, outType, tags, 0) 173 | } 174 | 175 | // writeCountedArray writes the metadata for an array field to the buffer. The 176 | // size of a counted array is fixed, and the size is written into the metadata 177 | // directly. 178 | // 179 | //nolint:unused // keep for future use 180 | func (em *eventMetadata) writeCountedArray(name string, count uint16, inType inType, outType outType, tags uint32) { 181 | em.writeFieldInner(name, inType|inTypeCountedArray, outType, tags, count) 182 | } 183 | 184 | // writeStruct writes the metadata for a nested struct to the buffer. The struct 185 | // contains the next N fields in the metadata, where N is specified by the 186 | // fieldCount argument. 187 | func (em *eventMetadata) writeStruct(name string, fieldCount uint8, tags uint32) { 188 | em.writeFieldInner(name, inTypeStruct, outType(fieldCount), tags, 0) 189 | } 190 | -------------------------------------------------------------------------------- /pkg/fs/resolve.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package fs 4 | 5 | import ( 6 | "errors" 7 | "os" 8 | "strings" 9 | 10 | "golang.org/x/sys/windows" 11 | 12 | "github.com/Microsoft/go-winio/internal/fs" 13 | ) 14 | 15 | // ResolvePath returns the final path to a file or directory represented, resolving symlinks, 16 | // handling mount points, etc. 17 | // The resolution works by using the Windows API GetFinalPathNameByHandle, which takes a 18 | // handle and returns the final path to that file. 19 | // 20 | // It is intended to address short-comings of [filepath.EvalSymlinks], which does not work 21 | // well on Windows. 22 | func ResolvePath(path string) (string, error) { 23 | h, err := openMetadata(path) 24 | if err != nil { 25 | return "", err 26 | } 27 | defer windows.CloseHandle(h) //nolint:errcheck 28 | 29 | // We use the Windows API GetFinalPathNameByHandle to handle path resolution. GetFinalPathNameByHandle 30 | // returns a resolved path name for a file or directory. The returned path can be in several different 31 | // formats, based on the flags passed. There are several goals behind the design here: 32 | // - Do as little manual path manipulation as possible. Since Windows path formatting can be quite 33 | // complex, we try to just let the Windows APIs handle that for us. 34 | // - Retain as much compatibility with existing Go path functions as we can. In particular, we try to 35 | // ensure paths returned from resolvePath can be passed to EvalSymlinks. 36 | // 37 | // First, we query for the VOLUME_NAME_GUID path of the file. This will return a path in the form 38 | // "\\?\Volume{8a25748f-cf34-4ac6-9ee2-c89400e886db}\dir\file.txt". If the path is a UNC share 39 | // (e.g. "\\server\share\dir\file.txt"), then the VOLUME_NAME_GUID query will fail with ERROR_PATH_NOT_FOUND. 40 | // In this case, we will next try a VOLUME_NAME_DOS query. This query will return a path for a UNC share 41 | // in the form "\\?\UNC\server\share\dir\file.txt". This path will work with most functions, but EvalSymlinks 42 | // fails on it. Therefore, we rewrite the path to the form "\\server\share\dir\file.txt" before returning it. 43 | // This path rewrite may not be valid in all cases (see the notes in the next paragraph), but those should 44 | // be very rare edge cases, and this case wouldn't have worked with EvalSymlinks anyways. 45 | // 46 | // The "\\?\" prefix indicates that no path parsing or normalization should be performed by Windows. 47 | // Instead the path is passed directly to the object manager. The lack of parsing means that "." and ".." are 48 | // interpreted literally and "\"" must be used as a path separator. Additionally, because normalization is 49 | // not done, certain paths can only be represented in this format. For instance, "\\?\C:\foo." (with a trailing .) 50 | // cannot be written as "C:\foo.", because path normalization will remove the trailing ".". 51 | // 52 | // FILE_NAME_NORMALIZED can fail on some UNC paths based on access restrictions. 53 | // Attempt to query with FILE_NAME_NORMALIZED, and then fall back on FILE_NAME_OPENED if access is denied. 54 | // 55 | // Querying for VOLUME_NAME_DOS first instead of VOLUME_NAME_GUID would yield a "nicer looking" path in some cases. 56 | // For instance, it could return "\\?\C:\dir\file.txt" instead of "\\?\Volume{8a25748f-cf34-4ac6-9ee2-c89400e886db}\dir\file.txt". 57 | // However, we query for VOLUME_NAME_GUID first for two reasons: 58 | // - The volume GUID path is more stable. A volume's mount point can change when it is remounted, but its 59 | // volume GUID should not change. 60 | // - If the volume is mounted at a non-drive letter path (e.g. mounted to "C:\mnt"), then VOLUME_NAME_DOS 61 | // will return the mount path. EvalSymlinks fails on a path like this due to a bug. 62 | // 63 | // References: 64 | // - GetFinalPathNameByHandle: https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfinalpathnamebyhandlea 65 | // - Naming Files, Paths, and Namespaces: https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file 66 | // - Naming a Volume: https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-volume 67 | 68 | normalize := true 69 | guid := true 70 | rPath := "" 71 | for i := 1; i <= 4; i++ { // maximum of 4 different cases to try 72 | var flags fs.GetFinalPathFlag 73 | if normalize { 74 | flags |= fs.FILE_NAME_NORMALIZED // nop; for clarity 75 | } else { 76 | flags |= fs.FILE_NAME_OPENED 77 | } 78 | 79 | if guid { 80 | flags |= fs.VOLUME_NAME_GUID 81 | } else { 82 | flags |= fs.VOLUME_NAME_DOS // nop; for clarity 83 | } 84 | 85 | rPath, err = fs.GetFinalPathNameByHandle(h, flags) 86 | switch { 87 | case guid && errors.Is(err, windows.ERROR_PATH_NOT_FOUND): 88 | // ERROR_PATH_NOT_FOUND is returned from the VOLUME_NAME_GUID query if the path is a 89 | // network share (UNC path). In this case, query for the DOS name instead. 90 | guid = false 91 | continue 92 | case normalize && errors.Is(err, windows.ERROR_ACCESS_DENIED): 93 | // normalization failed when accessing individual components along path for SMB share 94 | normalize = false 95 | continue 96 | default: 97 | } 98 | break 99 | } 100 | 101 | if err == nil && strings.HasPrefix(rPath, `\\?\UNC\`) { 102 | // Convert \\?\UNC\server\share -> \\server\share. The \\?\UNC syntax does not work with 103 | // some Go filepath functions such as EvalSymlinks. In the future if other components 104 | // move away from EvalSymlinks and use GetFinalPathNameByHandle instead, we could remove 105 | // this path munging. 106 | rPath = `\\` + rPath[len(`\\?\UNC\`):] 107 | } 108 | return rPath, err 109 | } 110 | 111 | // openMetadata takes a path, opens it with only meta-data access, and returns the resulting handle. 112 | // It works for both file and directory paths. 113 | func openMetadata(path string) (windows.Handle, error) { 114 | // We are not able to use builtin Go functionality for opening a directory path: 115 | // - os.Open on a directory returns a os.File where Fd() is a search handle from FindFirstFile. 116 | // - syscall.Open does not provide a way to specify FILE_FLAG_BACKUP_SEMANTICS, which is needed to 117 | // open a directory. 118 | // 119 | // We could use os.Open if the path is a file, but it's easier to just use the same code for both. 120 | // Therefore, we call windows.CreateFile directly. 121 | h, err := fs.CreateFile( 122 | path, 123 | fs.FILE_ANY_ACCESS, 124 | fs.FILE_SHARE_READ|fs.FILE_SHARE_WRITE|fs.FILE_SHARE_DELETE, 125 | nil, // security attributes 126 | fs.OPEN_EXISTING, 127 | fs.FILE_FLAG_BACKUP_SEMANTICS, // Needed to open a directory handle. 128 | fs.NullHandle, 129 | ) 130 | 131 | if err != nil { 132 | return 0, &os.PathError{ 133 | Op: "CreateFile", 134 | Path: path, 135 | Err: err, 136 | } 137 | } 138 | return h, nil 139 | } 140 | -------------------------------------------------------------------------------- /pkg/guid/guid.go: -------------------------------------------------------------------------------- 1 | // Package guid provides a GUID type. The backing structure for a GUID is 2 | // identical to that used by the golang.org/x/sys/windows GUID type. 3 | // There are two main binary encodings used for a GUID, the big-endian encoding, 4 | // and the Windows (mixed-endian) encoding. See here for details: 5 | // https://en.wikipedia.org/wiki/Universally_unique_identifier#Encoding 6 | package guid 7 | 8 | import ( 9 | "crypto/rand" 10 | "crypto/sha1" //nolint:gosec // not used for secure application 11 | "encoding" 12 | "encoding/binary" 13 | "fmt" 14 | "strconv" 15 | ) 16 | 17 | //go:generate go run golang.org/x/tools/cmd/stringer -type=Variant -trimprefix=Variant -linecomment 18 | 19 | // Variant specifies which GUID variant (or "type") of the GUID. It determines 20 | // how the entirety of the rest of the GUID is interpreted. 21 | type Variant uint8 22 | 23 | // The variants specified by RFC 4122 section 4.1.1. 24 | const ( 25 | // VariantUnknown specifies a GUID variant which does not conform to one of 26 | // the variant encodings specified in RFC 4122. 27 | VariantUnknown Variant = iota 28 | VariantNCS 29 | VariantRFC4122 // RFC 4122 30 | VariantMicrosoft 31 | VariantFuture 32 | ) 33 | 34 | // Version specifies how the bits in the GUID were generated. For instance, a 35 | // version 4 GUID is randomly generated, and a version 5 is generated from the 36 | // hash of an input string. 37 | type Version uint8 38 | 39 | func (v Version) String() string { 40 | return strconv.FormatUint(uint64(v), 10) 41 | } 42 | 43 | var _ = (encoding.TextMarshaler)(GUID{}) 44 | var _ = (encoding.TextUnmarshaler)(&GUID{}) 45 | 46 | // NewV4 returns a new version 4 (pseudorandom) GUID, as defined by RFC 4122. 47 | func NewV4() (GUID, error) { 48 | var b [16]byte 49 | if _, err := rand.Read(b[:]); err != nil { 50 | return GUID{}, err 51 | } 52 | 53 | g := FromArray(b) 54 | g.setVersion(4) // Version 4 means randomly generated. 55 | g.setVariant(VariantRFC4122) 56 | 57 | return g, nil 58 | } 59 | 60 | // NewV5 returns a new version 5 (generated from a string via SHA-1 hashing) 61 | // GUID, as defined by RFC 4122. The RFC is unclear on the encoding of the name, 62 | // and the sample code treats it as a series of bytes, so we do the same here. 63 | // 64 | // Some implementations, such as those found on Windows, treat the name as a 65 | // big-endian UTF16 stream of bytes. If that is desired, the string can be 66 | // encoded as such before being passed to this function. 67 | func NewV5(namespace GUID, name []byte) (GUID, error) { 68 | b := sha1.New() //nolint:gosec // not used for secure application 69 | namespaceBytes := namespace.ToArray() 70 | b.Write(namespaceBytes[:]) 71 | b.Write(name) 72 | 73 | a := [16]byte{} 74 | copy(a[:], b.Sum(nil)) 75 | 76 | g := FromArray(a) 77 | g.setVersion(5) // Version 5 means generated from a string. 78 | g.setVariant(VariantRFC4122) 79 | 80 | return g, nil 81 | } 82 | 83 | func fromArray(b [16]byte, order binary.ByteOrder) GUID { 84 | var g GUID 85 | g.Data1 = order.Uint32(b[0:4]) 86 | g.Data2 = order.Uint16(b[4:6]) 87 | g.Data3 = order.Uint16(b[6:8]) 88 | copy(g.Data4[:], b[8:16]) 89 | return g 90 | } 91 | 92 | func (g GUID) toArray(order binary.ByteOrder) [16]byte { 93 | b := [16]byte{} 94 | order.PutUint32(b[0:4], g.Data1) 95 | order.PutUint16(b[4:6], g.Data2) 96 | order.PutUint16(b[6:8], g.Data3) 97 | copy(b[8:16], g.Data4[:]) 98 | return b 99 | } 100 | 101 | // FromArray constructs a GUID from a big-endian encoding array of 16 bytes. 102 | func FromArray(b [16]byte) GUID { 103 | return fromArray(b, binary.BigEndian) 104 | } 105 | 106 | // ToArray returns an array of 16 bytes representing the GUID in big-endian 107 | // encoding. 108 | func (g GUID) ToArray() [16]byte { 109 | return g.toArray(binary.BigEndian) 110 | } 111 | 112 | // FromWindowsArray constructs a GUID from a Windows encoding array of bytes. 113 | func FromWindowsArray(b [16]byte) GUID { 114 | return fromArray(b, binary.LittleEndian) 115 | } 116 | 117 | // ToWindowsArray returns an array of 16 bytes representing the GUID in Windows 118 | // encoding. 119 | func (g GUID) ToWindowsArray() [16]byte { 120 | return g.toArray(binary.LittleEndian) 121 | } 122 | 123 | func (g GUID) String() string { 124 | return fmt.Sprintf( 125 | "%08x-%04x-%04x-%04x-%012x", 126 | g.Data1, 127 | g.Data2, 128 | g.Data3, 129 | g.Data4[:2], 130 | g.Data4[2:]) 131 | } 132 | 133 | // FromString parses a string containing a GUID and returns the GUID. The only 134 | // format currently supported is the `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` 135 | // format. 136 | func FromString(s string) (GUID, error) { 137 | if len(s) != 36 { 138 | return GUID{}, fmt.Errorf("invalid GUID %q", s) 139 | } 140 | if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' { 141 | return GUID{}, fmt.Errorf("invalid GUID %q", s) 142 | } 143 | 144 | var g GUID 145 | 146 | data1, err := strconv.ParseUint(s[0:8], 16, 32) 147 | if err != nil { 148 | return GUID{}, fmt.Errorf("invalid GUID %q", s) 149 | } 150 | g.Data1 = uint32(data1) 151 | 152 | data2, err := strconv.ParseUint(s[9:13], 16, 16) 153 | if err != nil { 154 | return GUID{}, fmt.Errorf("invalid GUID %q", s) 155 | } 156 | g.Data2 = uint16(data2) 157 | 158 | data3, err := strconv.ParseUint(s[14:18], 16, 16) 159 | if err != nil { 160 | return GUID{}, fmt.Errorf("invalid GUID %q", s) 161 | } 162 | g.Data3 = uint16(data3) 163 | 164 | for i, x := range []int{19, 21, 24, 26, 28, 30, 32, 34} { 165 | v, err := strconv.ParseUint(s[x:x+2], 16, 8) 166 | if err != nil { 167 | return GUID{}, fmt.Errorf("invalid GUID %q", s) 168 | } 169 | g.Data4[i] = uint8(v) 170 | } 171 | 172 | return g, nil 173 | } 174 | 175 | func (g *GUID) setVariant(v Variant) { 176 | d := g.Data4[0] 177 | switch v { 178 | case VariantNCS: 179 | d = (d & 0x7f) 180 | case VariantRFC4122: 181 | d = (d & 0x3f) | 0x80 182 | case VariantMicrosoft: 183 | d = (d & 0x1f) | 0xc0 184 | case VariantFuture: 185 | d = (d & 0x0f) | 0xe0 186 | case VariantUnknown: 187 | fallthrough 188 | default: 189 | panic(fmt.Sprintf("invalid variant: %d", v)) 190 | } 191 | g.Data4[0] = d 192 | } 193 | 194 | // Variant returns the GUID variant, as defined in RFC 4122. 195 | func (g GUID) Variant() Variant { 196 | b := g.Data4[0] 197 | if b&0x80 == 0 { 198 | return VariantNCS 199 | } else if b&0xc0 == 0x80 { 200 | return VariantRFC4122 201 | } else if b&0xe0 == 0xc0 { 202 | return VariantMicrosoft 203 | } else if b&0xe0 == 0xe0 { 204 | return VariantFuture 205 | } 206 | return VariantUnknown 207 | } 208 | 209 | func (g *GUID) setVersion(v Version) { 210 | g.Data3 = (g.Data3 & 0x0fff) | (uint16(v) << 12) 211 | } 212 | 213 | // Version returns the GUID version, as defined in RFC 4122. 214 | func (g GUID) Version() Version { 215 | return Version((g.Data3 & 0xF000) >> 12) 216 | } 217 | 218 | // MarshalText returns the textual representation of the GUID. 219 | func (g GUID) MarshalText() ([]byte, error) { 220 | return []byte(g.String()), nil 221 | } 222 | 223 | // UnmarshalText takes the textual representation of a GUID, and unmarhals it 224 | // into this GUID. 225 | func (g *GUID) UnmarshalText(text []byte) error { 226 | g2, err := FromString(string(text)) 227 | if err != nil { 228 | return err 229 | } 230 | *g = g2 231 | return nil 232 | } 233 | -------------------------------------------------------------------------------- /backuptar/tar_test.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package backuptar 4 | 5 | import ( 6 | "archive/tar" 7 | "bytes" 8 | "io" 9 | "os" 10 | "path/filepath" 11 | "reflect" 12 | "testing" 13 | 14 | "github.com/Microsoft/go-winio" 15 | "golang.org/x/sys/windows" 16 | ) 17 | 18 | func ensurePresent(t *testing.T, m map[string]string, keys ...string) { 19 | t.Helper() 20 | 21 | for _, k := range keys { 22 | if _, ok := m[k]; !ok { 23 | t.Error(k, "not present in tar header") 24 | } 25 | } 26 | } 27 | 28 | func setSparse(t *testing.T, f *os.File) { 29 | t.Helper() 30 | 31 | if err := windows.DeviceIoControl(windows.Handle(f.Fd()), windows.FSCTL_SET_SPARSE, nil, 0, nil, 0, nil, nil); err != nil { 32 | t.Fatal(err) 33 | } 34 | } 35 | 36 | // compareReaders validates that two readers contain the exact same data. 37 | func compareReaders(t *testing.T, rActual io.Reader, rExpected io.Reader) { 38 | t.Helper() 39 | 40 | const size = 8 * 1024 41 | var bufExpected, bufActual [size]byte 42 | var readCount int64 43 | // Loop, first reading from rExpected, then reading the same amount from rActual. 44 | // For each set of reads, compare the bytes to make sure they are identical. 45 | // When we run out of data in rExpected, exit the loop. 46 | for { 47 | // Do a read from rExpected and see how many bytes we get. 48 | nExpected, err := rExpected.Read(bufExpected[:]) 49 | if err == io.EOF && nExpected == 0 { 50 | break 51 | } else if err != nil && err != io.EOF { 52 | t.Fatalf("Failed reading from rExpected at %d: %s", readCount, err) 53 | } 54 | // Do a ReadFull from rActual for the same number of bytes we got from rExpected. 55 | if nActual, err := io.ReadFull(rActual, bufActual[:nExpected]); err != nil { 56 | t.Fatalf("Only read %d bytes out of %d from rActual at %d: %s", nActual, nExpected, readCount, err) 57 | } 58 | readCount += int64(nExpected) 59 | for i, bExpected := range bufExpected[:nExpected] { 60 | if bExpected != bufActual[i] { 61 | t.Fatalf("Mismatched bytes at %d. got 0x%x, expected 0x%x", i, bufActual[i], bExpected) 62 | } 63 | } 64 | } 65 | // Now we just need to make sure there isn't any further data in rActual. 66 | var b [1]byte 67 | if n, err := rActual.Read(b[:]); n != 0 || err != io.EOF { 68 | t.Fatalf("rActual didn't return EOF at expected end. Read %d bytes with error %s", n, err) 69 | } 70 | } 71 | 72 | func TestRoundTrip(t *testing.T) { 73 | // Each test case is a name mapped to a function which must create a file and return its path. 74 | // The test then round-trips that file through backuptar, and validates the output matches the input. 75 | // 76 | //nolint:gosec // G306: Expect WriteFile permissions to be 0600 or less 77 | for name, setup := range map[string]func(*testing.T) string{ 78 | "normalFile": func(t *testing.T) string { 79 | t.Helper() 80 | 81 | path := filepath.Join(t.TempDir(), "foo.txt") 82 | if err := os.WriteFile(path, []byte("testing 1 2 3\n"), 0644); err != nil { 83 | t.Fatal(err) 84 | } 85 | return path 86 | }, 87 | "normalFileEmpty": func(t *testing.T) string { 88 | t.Helper() 89 | 90 | path := filepath.Join(t.TempDir(), "foo.txt") 91 | f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644) 92 | if err != nil { 93 | t.Fatal(err) 94 | } 95 | defer f.Close() 96 | return path 97 | }, 98 | "sparseFileEmpty": func(t *testing.T) string { 99 | t.Helper() 100 | 101 | path := filepath.Join(t.TempDir(), "foo.txt") 102 | f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644) 103 | if err != nil { 104 | t.Fatal(err) 105 | } 106 | defer f.Close() 107 | setSparse(t, f) 108 | return path 109 | }, 110 | "sparseFileWithNoAllocatedRanges": func(t *testing.T) string { 111 | t.Helper() 112 | 113 | path := filepath.Join(t.TempDir(), "foo.txt") 114 | f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644) 115 | if err != nil { 116 | t.Fatal(err) 117 | } 118 | defer f.Close() 119 | setSparse(t, f) 120 | // Set file size without writing data to produce a file with size > 0 121 | // but no allocated ranges. 122 | if err := f.Truncate(1000000); err != nil { 123 | t.Fatal(err) 124 | } 125 | return path 126 | }, 127 | "sparseFileWithOneAllocatedRange": func(t *testing.T) string { 128 | t.Helper() 129 | 130 | path := filepath.Join(t.TempDir(), "foo.txt") 131 | f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644) 132 | if err != nil { 133 | t.Fatal(err) 134 | } 135 | defer f.Close() 136 | setSparse(t, f) 137 | if _, err := f.WriteString("test sparse data"); err != nil { 138 | t.Fatal(err) 139 | } 140 | return path 141 | }, 142 | "sparseFileWithMultipleAllocatedRanges": func(t *testing.T) string { 143 | t.Helper() 144 | 145 | path := filepath.Join(t.TempDir(), "foo.txt") 146 | f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644) 147 | if err != nil { 148 | t.Fatal(err) 149 | } 150 | defer f.Close() 151 | setSparse(t, f) 152 | if _, err = f.Write([]byte("testing 1 2 3\n")); err != nil { 153 | t.Fatal(err) 154 | } 155 | // The documentation talks about FSCTL_SET_ZERO_DATA, but seeking also 156 | // seems to create a hole. 157 | if _, err = f.Seek(1000000, 0); err != nil { 158 | t.Fatal(err) 159 | } 160 | if _, err = f.Write([]byte("more data later\n")); err != nil { 161 | t.Fatal(err) 162 | } 163 | return path 164 | }, 165 | } { 166 | t.Run(name, func(t *testing.T) { 167 | path := setup(t) 168 | f, err := os.Open(path) 169 | if err != nil { 170 | t.Fatal(err) 171 | } 172 | defer f.Close() 173 | 174 | fi, err := f.Stat() 175 | if err != nil { 176 | t.Fatal(err) 177 | } 178 | bi, err := winio.GetFileBasicInfo(f) 179 | if err != nil { 180 | t.Fatal(err) 181 | } 182 | 183 | br := winio.NewBackupFileReader(f, true) 184 | defer br.Close() 185 | var buf bytes.Buffer 186 | tw := tar.NewWriter(&buf) 187 | err = WriteTarFileFromBackupStream(tw, br, f.Name(), fi.Size(), bi) 188 | if err != nil { 189 | t.Fatal(err) 190 | } 191 | tr := tar.NewReader(&buf) 192 | hdr, err := tr.Next() 193 | if err != nil { 194 | t.Fatal(err) 195 | } 196 | 197 | name, size, bi2, err := FileInfoFromHeader(hdr) 198 | if err != nil { 199 | t.Fatal(err) 200 | } 201 | if name != filepath.ToSlash(f.Name()) { 202 | t.Errorf("got name %s, expected %s", name, filepath.ToSlash(f.Name())) 203 | } 204 | if size != fi.Size() { 205 | t.Errorf("got size %d, expected %d", size, fi.Size()) 206 | } 207 | if !reflect.DeepEqual(*bi2, *bi) { 208 | t.Errorf("got %#v, expected %#v", *bi2, *bi) 209 | } 210 | ensurePresent(t, hdr.PAXRecords, "MSWINDOWS.fileattr", "MSWINDOWS.rawsd") 211 | // Reset file position so we can compare file contents. 212 | // The file contents of the actual file should match what we get from the tar. 213 | if _, err := f.Seek(0, 0); err != nil { 214 | t.Fatal(err) 215 | } 216 | compareReaders(t, tr, f) 217 | }) 218 | } 219 | } 220 | 221 | func TestZeroReader(t *testing.T) { 222 | const size = 512 223 | var b [size]byte 224 | var bExpected [size]byte 225 | var r zeroReader 226 | n, err := r.Read(b[:]) 227 | if err != nil { 228 | t.Fatalf("Unexpected read error: %s", err) 229 | } 230 | if n != size { 231 | t.Errorf("Wrong read size. got %d, expected %d", n, size) 232 | } 233 | for i := range b { 234 | if b[i] != bExpected[i] { 235 | t.Errorf("Wrong content at index %d. got %d, expected %d", i, b[i], bExpected[i]) 236 | } 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /pkg/guid/guid_test.go: -------------------------------------------------------------------------------- 1 | package guid 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "testing" 7 | ) 8 | 9 | func mustNewV4(t *testing.T) GUID { 10 | t.Helper() 11 | 12 | g, err := NewV4() 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | return g 17 | } 18 | 19 | func mustNewV5(t *testing.T, namespace GUID, name []byte) GUID { 20 | t.Helper() 21 | 22 | g, err := NewV5(namespace, name) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | return g 27 | } 28 | 29 | func mustFromString(t *testing.T, s string) GUID { 30 | t.Helper() 31 | 32 | g, err := FromString(s) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | return g 37 | } 38 | 39 | func Test_Variant(t *testing.T) { 40 | type testCase struct { 41 | g GUID 42 | v Variant 43 | } 44 | testCases := []testCase{ 45 | {mustFromString(t, "f5cbc1a9-4cba-45a0-0fdd-b6761fc7dcc0"), VariantNCS}, 46 | {mustFromString(t, "f5cbc1a9-4cba-45a0-7fdd-b6761fc7dcc0"), VariantNCS}, 47 | {mustFromString(t, "f5cbc1a9-4cba-45a0-bfdd-b6761fc7dcc0"), VariantRFC4122}, 48 | {mustFromString(t, "f5cbc1a9-4cba-45a0-9fdd-b6761fc7dcc0"), VariantRFC4122}, 49 | {mustFromString(t, "f5cbc1a9-4cba-45a0-cfdd-b6761fc7dcc0"), VariantMicrosoft}, 50 | {mustFromString(t, "f5cbc1a9-4cba-45a0-dfdd-b6761fc7dcc0"), VariantMicrosoft}, 51 | {mustFromString(t, "f5cbc1a9-4cba-45a0-efdd-b6761fc7dcc0"), VariantFuture}, 52 | {mustFromString(t, "f5cbc1a9-4cba-45a0-ffdd-b6761fc7dcc0"), VariantFuture}, 53 | } 54 | for _, tc := range testCases { 55 | t.Run(tc.v.String()+"/"+tc.g.String(), func(t *testing.T) { 56 | actualVariant := tc.g.Variant() 57 | if actualVariant != tc.v { 58 | t.Fatalf("Variant is not correct.\nExpected: %d\nActual: %d\nGUID: %s", tc.v, actualVariant, tc.g) 59 | } 60 | }) 61 | } 62 | } 63 | 64 | func Test_SetVariant(t *testing.T) { 65 | g := mustFromString(t, "f5cbc1a9-4cba-45a0-bfdd-b6761fc7dcc0") 66 | for i := 0; i < len(_Variant_index)-1; i++ { 67 | v := Variant(i) 68 | if v == VariantUnknown { 69 | // Unknown is not a valid variant 70 | continue 71 | } 72 | t.Run(v.String(), func(t *testing.T) { 73 | g.setVariant(v) 74 | if g.Variant() != v { 75 | t.Fatalf("Variant is incorrect.\nExpected: %d\nActual: %d", v, g.Variant()) 76 | } 77 | }) 78 | } 79 | } 80 | 81 | func Test_Version(t *testing.T) { 82 | type testCase struct { 83 | g GUID 84 | v Version 85 | } 86 | testCases := []testCase{ 87 | {mustFromString(t, "f5cbc1a9-4cba-15a0-0fdd-b6761fc7dcc0"), 1}, 88 | {mustFromString(t, "f5cbc1a9-4cba-25a0-0fdd-b6761fc7dcc0"), 2}, 89 | {mustFromString(t, "f5cbc1a9-4cba-35a0-0fdd-b6761fc7dcc0"), 3}, 90 | {mustFromString(t, "f5cbc1a9-4cba-45a0-0fdd-b6761fc7dcc0"), 4}, 91 | {mustFromString(t, "f5cbc1a9-4cba-55a0-0fdd-b6761fc7dcc0"), 5}, 92 | } 93 | for _, tc := range testCases { 94 | t.Run(tc.v.String()+"-"+tc.g.String(), func(t *testing.T) { 95 | actualVersion := tc.g.Version() 96 | if actualVersion != tc.v { 97 | t.Fatalf("Version is not correct.\nExpected: %d\nActual: %d\nGUID: %s", tc.v, actualVersion, tc.g) 98 | } 99 | }) 100 | } 101 | } 102 | 103 | func Test_SetVersion(t *testing.T) { 104 | g := mustFromString(t, "f5cbc1a9-4cba-45a0-bfdd-b6761fc7dcc0") 105 | for tc := 0; tc < 16; tc++ { 106 | v := Version(tc) 107 | t.Run(v.String(), func(t *testing.T) { 108 | g.setVersion(v) 109 | if g.Version() != v { 110 | t.Fatalf("Version is incorrect.\nExpected: %d\nActual: %d", v, g.Version()) 111 | } 112 | }) 113 | } 114 | } 115 | 116 | func Test_NewV4IsUnique(t *testing.T) { 117 | g := mustNewV4(t) 118 | g2 := mustNewV4(t) 119 | if g == g2 { 120 | t.Fatalf("GUIDs are equal: %s, %s", g, g2) 121 | } 122 | } 123 | 124 | func Test_V4HasCorrectVersionAndVariant(t *testing.T) { 125 | g := mustNewV4(t) 126 | if g.Version() != 4 { 127 | t.Fatalf("Version is not 4: %s", g) 128 | } 129 | if g.Variant() != VariantRFC4122 { 130 | t.Fatalf("Variant is not RFC4122: %s", g) 131 | } 132 | } 133 | 134 | func Test_V5HasCorrectVersionAndVariant(t *testing.T) { 135 | namespace := mustFromString(t, "f5cbc1a9-4cba-45a0-bfdd-b6761fc7dcc0") 136 | g := mustNewV5(t, namespace, []byte("Foo")) 137 | if g.Version() != 5 { 138 | t.Fatalf("Version is not 5: %s", g) 139 | } 140 | if g.Variant() != VariantRFC4122 { 141 | t.Fatalf("Variant is not RFC4122: %s", g) 142 | } 143 | } 144 | 145 | func Test_V5KnownValues(t *testing.T) { 146 | type testCase struct { 147 | ns GUID 148 | name string 149 | g GUID 150 | } 151 | testCases := []testCase{ 152 | { 153 | mustFromString(t, "6ba7b810-9dad-11d1-80b4-00c04fd430c8"), 154 | "www.sample.com", 155 | mustFromString(t, "4e4463eb-b0e8-54fa-8c28-12d1ab1d45b3"), 156 | }, 157 | { 158 | mustFromString(t, "6ba7b811-9dad-11d1-80b4-00c04fd430c8"), 159 | "https://www.sample.com/test", 160 | mustFromString(t, "9e44625a-0d85-5e0a-99bc-8e8a77df5ea2"), 161 | }, 162 | { 163 | mustFromString(t, "6ba7b812-9dad-11d1-80b4-00c04fd430c8"), 164 | "1.3.6.1.4.1.343", 165 | mustFromString(t, "6aab0456-7392-582a-b92a-ba5a7096945d"), 166 | }, 167 | { 168 | mustFromString(t, "6ba7b814-9dad-11d1-80b4-00c04fd430c8"), 169 | "CN=John Smith, ou=People, o=FakeCorp, L=Seattle, S=Washington, C=US", 170 | mustFromString(t, "badff8dd-c869-5b64-a260-00092e66be00"), 171 | }, 172 | } 173 | for _, tc := range testCases { 174 | t.Run(tc.name, func(t *testing.T) { 175 | g := mustNewV5(t, tc.ns, []byte(tc.name)) 176 | if g != tc.g { 177 | t.Fatalf("GUIDs are not equal.\nExpected: %s\nActual: %s", tc.g, g) 178 | } 179 | }) 180 | } 181 | } 182 | 183 | func Test_ToArray(t *testing.T) { 184 | g := mustFromString(t, "73c39589-192e-4c64-9acf-6c5d0aa18528") 185 | b := g.ToArray() 186 | expected := [16]byte{0x73, 0xc3, 0x95, 0x89, 0x19, 0x2e, 0x4c, 0x64, 0x9a, 0xcf, 0x6c, 0x5d, 0x0a, 0xa1, 0x85, 0x28} 187 | if b != expected { 188 | t.Fatalf("GUID does not match array form: %x, %x", expected, b) 189 | } 190 | } 191 | 192 | func Test_FromArrayAndBack(t *testing.T) { 193 | b := [16]byte{0x73, 0xc3, 0x95, 0x89, 0x19, 0x2e, 0x4c, 0x64, 0x9a, 0xcf, 0x6c, 0x5d, 0x0a, 0xa1, 0x85, 0x28} 194 | b2 := FromArray(b).ToArray() 195 | if b != b2 { 196 | t.Fatalf("Arrays do not match: %x, %x", b, b2) 197 | } 198 | } 199 | 200 | func Test_ToWindowsArray(t *testing.T) { 201 | g := mustFromString(t, "73c39589-192e-4c64-9acf-6c5d0aa18528") 202 | b := g.ToWindowsArray() 203 | expected := [16]byte{0x89, 0x95, 0xc3, 0x73, 0x2e, 0x19, 0x64, 0x4c, 0x9a, 0xcf, 0x6c, 0x5d, 0x0a, 0xa1, 0x85, 0x28} 204 | if b != expected { 205 | t.Fatalf("GUID does not match array form: %x, %x", expected, b) 206 | } 207 | } 208 | 209 | func Test_FromWindowsArrayAndBack(t *testing.T) { 210 | b := [16]byte{0x73, 0xc3, 0x95, 0x89, 0x19, 0x2e, 0x4c, 0x64, 0x9a, 0xcf, 0x6c, 0x5d, 0x0a, 0xa1, 0x85, 0x28} 211 | b2 := FromWindowsArray(b).ToWindowsArray() 212 | if b != b2 { 213 | t.Fatalf("Arrays do not match: %x, %x", b, b2) 214 | } 215 | } 216 | 217 | func Test_FromString(t *testing.T) { 218 | orig := "8e35239e-2084-490e-a3db-ab18ee0744cb" 219 | g := mustFromString(t, orig) 220 | s := g.String() 221 | if orig != s { 222 | t.Fatalf("GUIDs not equal: %s, %s", orig, s) 223 | } 224 | } 225 | 226 | func Test_MarshalJSON(t *testing.T) { 227 | g := mustNewV4(t) 228 | j, err := json.Marshal(g) 229 | if err != nil { 230 | t.Fatal(err) 231 | } 232 | gj := fmt.Sprintf("\"%s\"", g.String()) 233 | if string(j) != gj { 234 | t.Fatalf("JSON not equal: %s, %s", j, gj) 235 | } 236 | } 237 | 238 | func Test_MarshalJSON_Nested(t *testing.T) { 239 | type test struct { 240 | G GUID 241 | } 242 | g := mustNewV4(t) 243 | t1 := test{g} 244 | j, err := json.Marshal(t1) 245 | if err != nil { 246 | t.Fatal(err) 247 | } 248 | gj := fmt.Sprintf("{\"G\":\"%s\"}", g.String()) 249 | if string(j) != gj { 250 | t.Fatalf("JSON not equal: %s, %s", j, gj) 251 | } 252 | } 253 | 254 | func Test_UnmarshalJSON(t *testing.T) { 255 | g := mustNewV4(t) 256 | j, err := json.Marshal(g) 257 | if err != nil { 258 | t.Fatal(err) 259 | } 260 | var g2 GUID 261 | if err := json.Unmarshal(j, &g2); err != nil { 262 | t.Fatal(err) 263 | } 264 | if g != g2 { 265 | t.Fatalf("GUIDs not equal: %s, %s", g, g2) 266 | } 267 | } 268 | 269 | func Test_UnmarshalJSON_Nested(t *testing.T) { 270 | type test struct { 271 | G GUID 272 | } 273 | g := mustNewV4(t) 274 | t1 := test{g} 275 | j, err := json.Marshal(t1) 276 | if err != nil { 277 | t.Fatal(err) 278 | } 279 | var t2 test 280 | if err := json.Unmarshal(j, &t2); err != nil { 281 | t.Fatal(err) 282 | } 283 | if t1.G != t2.G { 284 | t.Fatalf("GUIDs not equal: %v, %v", t1.G, t2.G) 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /pkg/fs/resolve_test.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package fs 4 | 5 | import ( 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | "syscall" 10 | "testing" 11 | 12 | "golang.org/x/sys/windows" 13 | 14 | "github.com/Microsoft/go-winio/internal/computestorage" 15 | "github.com/Microsoft/go-winio/internal/fs" 16 | "github.com/Microsoft/go-winio/vhd" 17 | ) 18 | 19 | func getWindowsBuildNumber() uint32 { 20 | // RtlGetVersion ignores manifest requirements 21 | vex := windows.RtlGetVersion() 22 | return vex.BuildNumber 23 | } 24 | 25 | func makeSymlink(t *testing.T, oldName string, newName string) { 26 | t.Helper() 27 | 28 | t.Logf("make symlink: %s -> %s", oldName, newName) 29 | 30 | if _, err := os.Lstat(oldName); err != nil { 31 | t.Fatalf("could not open file %q: %v", oldName, err) 32 | } 33 | 34 | if err := os.Symlink(oldName, newName); err != nil { 35 | t.Fatalf("creating symlink: %s", err) 36 | } 37 | 38 | if _, err := os.Lstat(newName); err != nil { 39 | t.Fatalf("could not open file %q: %v", newName, err) 40 | } 41 | } 42 | 43 | func getVolumeGUIDPath(t *testing.T, path string) string { 44 | t.Helper() 45 | 46 | h, err := openMetadata(path) 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | defer windows.CloseHandle(h) //nolint:errcheck 51 | final, err := fs.GetFinalPathNameByHandle(h, fs.FILE_NAME_OPENED|fs.VOLUME_NAME_GUID) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | return final 56 | } 57 | 58 | func openDisk(path string) (windows.Handle, error) { 59 | h, err := fs.CreateFile( 60 | path, 61 | windows.GENERIC_READ|windows.GENERIC_WRITE, 62 | fs.FILE_SHARE_READ|fs.FILE_SHARE_WRITE, 63 | nil, // security attributes 64 | fs.OPEN_EXISTING, 65 | windows.FILE_ATTRIBUTE_NORMAL|fs.FILE_FLAG_NO_BUFFERING, 66 | fs.NullHandle) 67 | if err != nil { 68 | return 0, &os.PathError{ 69 | Op: "CreateFile", 70 | Path: path, 71 | Err: err, 72 | } 73 | } 74 | return h, nil 75 | } 76 | 77 | func formatVHD(vhdHandle windows.Handle) error { 78 | h := vhdHandle 79 | // Pre-19H1 HcsFormatWritableLayerVhd expects a disk handle. 80 | // On newer builds it expects a VHD handle instead. 81 | // Open a handle to the VHD's disk object if needed. 82 | 83 | // Windows Server 1903, aka 19H1 84 | if getWindowsBuildNumber() < 18362 { 85 | diskPath, err := vhd.GetVirtualDiskPhysicalPath(syscall.Handle(h)) 86 | if err != nil { 87 | return err 88 | } 89 | diskHandle, err := openDisk(diskPath) 90 | if err != nil { 91 | return err 92 | } 93 | defer windows.CloseHandle(diskHandle) //nolint:errcheck // cleanup code 94 | h = diskHandle 95 | } 96 | // Formatting a disk directly in Windows is a pain, so we use FormatWritableLayerVhd to do it. 97 | // It has a side effect of creating a sandbox directory on the formatted volume, but it's safe 98 | // to just ignore that for our purposes here. 99 | return computestorage.FormatWritableLayerVHD(h) 100 | } 101 | 102 | // Creates a VHD with a NTFS volume. Returns the volume path. 103 | func setupVHDVolume(t *testing.T, vhdPath string) string { 104 | t.Helper() 105 | 106 | vhdHandle, err := vhd.CreateVirtualDisk(vhdPath, 107 | vhd.VirtualDiskAccessNone, vhd.CreateVirtualDiskFlagNone, 108 | &vhd.CreateVirtualDiskParameters{ 109 | Version: 2, 110 | Version2: vhd.CreateVersion2{ 111 | MaximumSize: 5 * 1024 * 1024 * 1024, // 5GB, thin provisioned 112 | BlockSizeInBytes: 1 * 1024 * 1024, // 1MB 113 | }, 114 | }) 115 | if err != nil { 116 | t.Fatal(err) 117 | } 118 | t.Cleanup(func() { 119 | _ = windows.CloseHandle(windows.Handle(vhdHandle)) 120 | }) 121 | if err := vhd.AttachVirtualDisk(vhdHandle, vhd.AttachVirtualDiskFlagNone, &vhd.AttachVirtualDiskParameters{Version: 1}); err != nil { 122 | t.Fatal(err) 123 | } 124 | t.Cleanup(func() { 125 | if err := vhd.DetachVirtualDisk(vhdHandle); err != nil { 126 | t.Fatal(err) 127 | } 128 | }) 129 | if err := formatVHD(windows.Handle(vhdHandle)); err != nil { 130 | t.Fatalf("failed to format VHD: %s", err) 131 | } 132 | // Get the path for the volume that was just created on the disk. 133 | volumePath, err := computestorage.GetLayerVHDMountPath(windows.Handle(vhdHandle)) 134 | if err != nil { 135 | t.Fatal(err) 136 | } 137 | return volumePath 138 | } 139 | 140 | func writeFile(t *testing.T, path string, content []byte) { 141 | t.Helper() 142 | 143 | if err := os.WriteFile(path, content, 0644); err != nil { //nolint:gosec // test file, can have permissive mode 144 | t.Fatal(err) 145 | } 146 | } 147 | 148 | func mountVolume(t *testing.T, volumePath string, mountPoint string) { 149 | t.Helper() 150 | 151 | // Create the mount point directory. 152 | if err := os.Mkdir(mountPoint, 0644); err != nil { 153 | t.Fatal(err) 154 | } 155 | t.Cleanup(func() { 156 | if err := os.Remove(mountPoint); err != nil { 157 | t.Fatal(err) 158 | } 159 | }) 160 | // Volume path must end in a slash. 161 | if !strings.HasSuffix(volumePath, `\`) { 162 | volumePath += `\` 163 | } 164 | volumePathU16, err := windows.UTF16PtrFromString(volumePath) 165 | if err != nil { 166 | t.Fatal(err) 167 | } 168 | // Mount point must end in a slash. 169 | if !strings.HasSuffix(mountPoint, `\`) { 170 | mountPoint += `\` 171 | } 172 | mountPointU16, err := windows.UTF16PtrFromString(mountPoint) 173 | if err != nil { 174 | t.Fatal(err) 175 | } 176 | if err := windows.SetVolumeMountPoint(mountPointU16, volumePathU16); err != nil { 177 | t.Fatalf("failed to mount %s onto %s: %s", volumePath, mountPoint, err) 178 | } 179 | t.Cleanup(func() { 180 | if err := windows.DeleteVolumeMountPoint(mountPointU16); err != nil { 181 | t.Fatalf("failed to delete mount on %s: %s", mountPoint, err) 182 | } 183 | }) 184 | } 185 | 186 | func TestResolvePath(t *testing.T) { 187 | if !windows.GetCurrentProcessToken().IsElevated() { 188 | t.Skip("requires elevated privileges") 189 | } 190 | 191 | // Set up some data to be used by the test cases. 192 | volumePathC := getVolumeGUIDPath(t, `C:\`) 193 | dir := t.TempDir() 194 | 195 | makeSymlink(t, `C:\windows`, filepath.Join(dir, "lnk1")) 196 | makeSymlink(t, `\\localhost\c$\windows`, filepath.Join(dir, "lnk2")) 197 | 198 | volumePathVHD1 := setupVHDVolume(t, filepath.Join(dir, "foo.vhdx")) 199 | writeFile(t, filepath.Join(volumePathVHD1, "data.txt"), []byte("test content 1")) 200 | makeSymlink(t, filepath.Join(volumePathVHD1, "data.txt"), filepath.Join(dir, "lnk3")) 201 | 202 | volumePathVHD2 := setupVHDVolume(t, filepath.Join(dir, "bar.vhdx")) 203 | writeFile(t, filepath.Join(volumePathVHD2, "data.txt"), []byte("test content 2")) 204 | makeSymlink(t, filepath.Join(volumePathVHD2, "data.txt"), filepath.Join(dir, "lnk4")) 205 | mountVolume(t, volumePathVHD2, filepath.Join(dir, "mnt")) 206 | 207 | for _, tc := range []struct { 208 | input string 209 | expected string 210 | description string 211 | }{ 212 | {`C:\windows`, volumePathC + `Windows`, "local path"}, 213 | {filepath.Join(dir, "lnk1"), volumePathC + `Windows`, "symlink to local path"}, 214 | {`\\localhost\c$\windows`, `\\localhost\c$\Windows`, "UNC path"}, 215 | {filepath.Join(dir, "lnk2"), `\\localhost\c$\Windows`, "symlink to UNC path"}, 216 | {filepath.Join(volumePathVHD1, "data.txt"), filepath.Join(volumePathVHD1, "data.txt"), "volume with no mount point"}, 217 | {filepath.Join(dir, "lnk3"), filepath.Join(volumePathVHD1, "data.txt"), "symlink to volume with no mount point"}, 218 | {filepath.Join(dir, "mnt", "data.txt"), filepath.Join(volumePathVHD2, "data.txt"), "volume with mount point"}, 219 | {filepath.Join(dir, "lnk4"), filepath.Join(volumePathVHD2, "data.txt"), "symlink to volume with mount point"}, 220 | } { 221 | t.Run(tc.description, func(t *testing.T) { 222 | t.Logf("resolving: %s -> %s", tc.input, tc.expected) 223 | 224 | actual, err := ResolvePath(tc.input) 225 | if err != nil { 226 | t.Fatalf("ResolvePath should return no error, but: %v", err) 227 | } 228 | if actual != tc.expected { 229 | t.Fatalf("expected %v but got %v", tc.expected, actual) 230 | } 231 | 232 | // Make sure EvalSymlinks works with the resolved path, as an extra safety measure. 233 | t.Logf("filepath.EvalSymlinks(%s)", actual) 234 | p, err := filepath.EvalSymlinks(actual) 235 | if err != nil { 236 | t.Fatalf("EvalSymlinks should return no error, but %v", err) 237 | } 238 | // As an extra-extra safety, check that resolvePath(x) == EvalSymlinks(resolvePath(x)). 239 | // EvalSymlinks normalizes UNC path casing, but resolvePath may not, so compare with 240 | // case-insensitivity here. 241 | if !strings.EqualFold(actual, p) { 242 | t.Fatalf("EvalSymlinks should resolve to the same path. Expected %v but got %v", actual, p) 243 | } 244 | }) 245 | } 246 | } 247 | --------------------------------------------------------------------------------