From 37925b3e7a96e93faa61d8251ca50b6ab918d694 Mon Sep 17 00:00:00 2001 From: Aaron Klotz Date: Mon, 26 Jun 2023 11:50:45 -0600 Subject: [PATCH] go.mod, cmd/tailscaled, ipn/localapi, util/osdiag, util/winutil, util/winutil/authenticode: add Windows module list to OS-specific logs that are written upon bugreport * We update wingoes to pick up new version information functionality (See pe/version.go in the https://github.com/dblohm7/wingoes repo); * We move the existing LogSupportInfo code (including necessary syscall stubs) out of util/winutil into a new package, util/osdiag, and implement the public LogSupportInfo function may be implemented for other platforms as needed; * We add a new reason argument to LogSupportInfo and wire that into localapi's bugreport implementation; * We add module information to the Windows implementation of LogSupportInfo when reason indicates a bugreport. We enumerate all loaded modules in our process, and for each one we gather debug, authenticode signature, and version information. Fixes #7802 Signed-off-by: Aaron Klotz --- cmd/tailscaled/depaware.txt | 10 +- cmd/tailscaled/tailscaled_windows.go | 3 +- go.mod | 2 +- go.sum | 4 +- ipn/localapi/localapi.go | 4 + .../tailscaled_deps_test_windows.go | 1 + util/osdiag/mksyscall.go | 9 + util/osdiag/osdiag.go | 23 ++ util/osdiag/osdiag_notwindows.go | 11 + util/osdiag/osdiag_windows.go | 330 ++++++++++++++++++ util/osdiag/osdiag_windows_test.go | 128 +++++++ util/osdiag/zsyscall_windows.go | 53 +++ .../authenticode/authenticode_windows.go | 3 + util/winutil/mksyscall.go | 1 - util/winutil/winutil_windows.go | 168 --------- util/winutil/winutil_windows_test.go | 120 ------- util/winutil/zsyscall_windows.go | 10 - 17 files changed, 575 insertions(+), 305 deletions(-) create mode 100644 util/osdiag/mksyscall.go create mode 100644 util/osdiag/osdiag.go create mode 100644 util/osdiag/osdiag_notwindows.go create mode 100644 util/osdiag/osdiag_windows.go create mode 100644 util/osdiag/osdiag_windows_test.go create mode 100644 util/osdiag/zsyscall_windows.go diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index 3c6394618..c5b6f7da7 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -77,9 +77,10 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de L github.com/aws/smithy-go/waiter from github.com/aws/aws-sdk-go-v2/service/ssm L github.com/coreos/go-iptables/iptables from tailscale.com/util/linuxfw LD 💣 github.com/creack/pty from tailscale.com/ssh/tailssh - W 💣 github.com/dblohm7/wingoes from github.com/dblohm7/wingoes/com + W 💣 github.com/dblohm7/wingoes from github.com/dblohm7/wingoes/com+ W 💣 github.com/dblohm7/wingoes/com from tailscale.com/cmd/tailscaled W github.com/dblohm7/wingoes/internal from github.com/dblohm7/wingoes/com + W 💣 github.com/dblohm7/wingoes/pe from tailscale.com/util/osdiag+ github.com/fxamacker/cbor/v2 from tailscale.com/tka W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+ W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet @@ -332,6 +333,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/util/mak from tailscale.com/control/controlclient+ tailscale.com/util/multierr from tailscale.com/control/controlclient+ tailscale.com/util/must from tailscale.com/logpolicy + 💣 tailscale.com/util/osdiag from tailscale.com/cmd/tailscaled+ tailscale.com/util/osshare from tailscale.com/ipn/ipnlocal+ W tailscale.com/util/pidowner from tailscale.com/ipn/ipnauth tailscale.com/util/racebuild from tailscale.com/logpolicy @@ -343,6 +345,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/util/systemd from tailscale.com/control/controlclient+ tailscale.com/util/uniq from tailscale.com/wgengine/magicsock+ 💣 tailscale.com/util/winutil from tailscale.com/control/controlclient+ + W 💣 tailscale.com/util/winutil/authenticode from tailscale.com/util/osdiag W tailscale.com/util/winutil/policy from tailscale.com/ipn/ipnlocal tailscale.com/version from tailscale.com/derp+ tailscale.com/version/distro from tailscale.com/hostinfo+ @@ -409,6 +412,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de bytes from bufio+ compress/flate from compress/gzip+ compress/gzip from golang.org/x/net/http2+ + W compress/zlib from debug/pe container/heap from gvisor.dev/gvisor/pkg/tcpip/transport/tcp container/list from crypto/tls+ context from crypto/tls+ @@ -433,6 +437,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de crypto/tls from github.com/tcnksm/go-httpstat+ crypto/x509 from crypto/tls+ crypto/x509/pkix from crypto/x509+ + W debug/dwarf from debug/pe + W debug/pe from github.com/dblohm7/wingoes/pe embed from tailscale.com+ encoding from encoding/json+ encoding/asn1 from crypto/x509+ @@ -448,7 +454,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de flag from net/http/httptest+ fmt from compress/flate+ hash from crypto+ - hash/adler32 from tailscale.com/ipn/ipnlocal + hash/adler32 from tailscale.com/ipn/ipnlocal+ hash/crc32 from compress/gzip+ hash/fnv from tailscale.com/wgengine/magicsock+ hash/maphash from go4.org/mem diff --git a/cmd/tailscaled/tailscaled_windows.go b/cmd/tailscaled/tailscaled_windows.go index d40622b37..0d056250e 100644 --- a/cmd/tailscaled/tailscaled_windows.go +++ b/cmd/tailscaled/tailscaled_windows.go @@ -50,6 +50,7 @@ import ( "tailscale.com/tsd" "tailscale.com/types/logger" "tailscale.com/types/logid" + "tailscale.com/util/osdiag" "tailscale.com/util/winutil" "tailscale.com/version" "tailscale.com/wf" @@ -127,7 +128,7 @@ var syslogf logger.Logf = logger.Discard // Windows started. func runWindowsService(pol *logpolicy.Policy) error { go func() { - winutil.LogSupportInfo(log.Printf) + osdiag.LogSupportInfo(logger.WithPrefix(log.Printf, "Support Info: "), osdiag.LogSupportInfoReasonStartup) }() if winutil.GetPolicyInteger("LogSCMInteractions", 0) != 0 { diff --git a/go.mod b/go.mod index c19dcc935..c9a080a21 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf github.com/creack/pty v1.1.18 github.com/dave/jennifer v1.6.1 - github.com/dblohm7/wingoes v0.0.0-20230801195049-ed8077baf0cd + github.com/dblohm7/wingoes v0.0.0-20230803162905-5c6286bb8c6e github.com/dsnet/try v0.0.3 github.com/evanw/esbuild v0.14.53 github.com/frankban/quicktest v1.14.5 diff --git a/go.sum b/go.sum index fe6b39f2b..e9642bcc1 100644 --- a/go.sum +++ b/go.sum @@ -240,8 +240,8 @@ github.com/dave/jennifer v1.6.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dblohm7/wingoes v0.0.0-20230801195049-ed8077baf0cd h1:zYVpYS5d3Uf04vVCJuzqpOCwQQIzJibtOx8ivt7zt2Q= -github.com/dblohm7/wingoes v0.0.0-20230801195049-ed8077baf0cd/go.mod h1:6NCrWM5jRefaG7iN0iMShPalLsljHWBh9v1zxM2f8Xs= +github.com/dblohm7/wingoes v0.0.0-20230803162905-5c6286bb8c6e h1:tTRuQNnXKO6Ffu62nk9bnnPx/m+IyNMdFFfzsETyRO8= +github.com/dblohm7/wingoes v0.0.0-20230803162905-5c6286bb8c6e/go.mod h1:6NCrWM5jRefaG7iN0iMShPalLsljHWBh9v1zxM2f8Xs= github.com/denis-tingaikin/go-header v0.4.3 h1:tEaZKAlqql6SKCY++utLmkPLd6K8IBM20Ha7UVm+mtU= github.com/denis-tingaikin/go-header v0.4.3/go.mod h1:0wOCWuN71D5qIgE2nz9KrKmuYBAC2Mra5RassOIQ2/c= github.com/denis-tingajkin/go-header v0.3.1/go.mod h1:sq/2IxMhaZX+RRcgHfCRx/m0M5na0fBt4/CRe7Lrji0= diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go index eda32ac57..9e4b41157 100644 --- a/ipn/localapi/localapi.go +++ b/ipn/localapi/localapi.go @@ -48,6 +48,7 @@ import ( "tailscale.com/util/clientmetric" "tailscale.com/util/httpm" "tailscale.com/util/mak" + "tailscale.com/util/osdiag" "tailscale.com/version" ) @@ -350,6 +351,9 @@ func (h *Handler) serveBugReport(w http.ResponseWriter, r *http.Request) { // logs for them. envknob.LogCurrent(logger.WithPrefix(h.logf, "user bugreport: ")) + // OS-specific details + osdiag.LogSupportInfo(logger.WithPrefix(h.logf, "user bugreport OS: "), osdiag.LogSupportInfoReasonBugReport) + if defBool(r.URL.Query().Get("diagnose"), false) { h.b.Doctor(r.Context(), logger.WithPrefix(h.logf, "diag: ")) } diff --git a/tstest/integration/tailscaled_deps_test_windows.go b/tstest/integration/tailscaled_deps_test_windows.go index 6eb00eff4..2ef5a002e 100644 --- a/tstest/integration/tailscaled_deps_test_windows.go +++ b/tstest/integration/tailscaled_deps_test_windows.go @@ -52,6 +52,7 @@ import ( _ "tailscale.com/types/logid" _ "tailscale.com/util/clientmetric" _ "tailscale.com/util/multierr" + _ "tailscale.com/util/osdiag" _ "tailscale.com/util/osshare" _ "tailscale.com/util/winutil" _ "tailscale.com/version" diff --git a/util/osdiag/mksyscall.go b/util/osdiag/mksyscall.go new file mode 100644 index 000000000..72e4475cb --- /dev/null +++ b/util/osdiag/mksyscall.go @@ -0,0 +1,9 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +package osdiag + +//go:generate go run golang.org/x/sys/windows/mkwinsyscall -output zsyscall_windows.go mksyscall.go +//go:generate go run golang.org/x/tools/cmd/goimports -w zsyscall_windows.go + +//sys regEnumValue(key registry.Key, index uint32, valueName *uint16, valueNameLen *uint32, reserved *uint32, valueType *uint32, pData *byte, cbData *uint32) (ret error) [failretval!=0] = advapi32.RegEnumValueW diff --git a/util/osdiag/osdiag.go b/util/osdiag/osdiag.go new file mode 100644 index 000000000..df1bcb362 --- /dev/null +++ b/util/osdiag/osdiag.go @@ -0,0 +1,23 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +// Package osdiag provides loggers for OS-specific diagnostic information. +package osdiag + +import "tailscale.com/types/logger" + +// LogSupportInfoReason is an enumeration indicating the reason for logging +// support info. +type LogSupportInfoReason int + +const ( + LogSupportInfoReasonStartup LogSupportInfoReason = iota + 1 // tailscaled is starting up. + LogSupportInfoReasonBugReport // a bugreport is in the process of being gathered. +) + +// LogSupportInfo obtains OS-specific diagnostic information useful for +// troubleshooting and support, and writes it to logf. The reason argument is +// useful for governing the verbosity of this function's output. +func LogSupportInfo(logf logger.Logf, reason LogSupportInfoReason) { + logSupportInfo(logf, reason) +} diff --git a/util/osdiag/osdiag_notwindows.go b/util/osdiag/osdiag_notwindows.go new file mode 100644 index 000000000..da172b967 --- /dev/null +++ b/util/osdiag/osdiag_notwindows.go @@ -0,0 +1,11 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +//go:build !windows + +package osdiag + +import "tailscale.com/types/logger" + +func logSupportInfo(logger.Logf, LogSupportInfoReason) { +} diff --git a/util/osdiag/osdiag_windows.go b/util/osdiag/osdiag_windows.go new file mode 100644 index 000000000..2b23df073 --- /dev/null +++ b/util/osdiag/osdiag_windows.go @@ -0,0 +1,330 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +package osdiag + +import ( + "encoding/binary" + "encoding/json" + "errors" + "fmt" + "io" + "path/filepath" + "strings" + "unicode/utf16" + "unsafe" + + "github.com/dblohm7/wingoes/pe" + "golang.org/x/sys/windows" + "golang.org/x/sys/windows/registry" + "tailscale.com/types/logger" + "tailscale.com/util/winutil" + "tailscale.com/util/winutil/authenticode" +) + +const ( + maxBinaryValueLen = 128 // we'll truncate any binary values longer than this + maxRegValueNameLen = 16384 // maximum length supported by Windows + 1 + initialValueBufLen = 80 // large enough to contain a stringified GUID encoded as UTF-16 +) + +func logSupportInfo(logf logger.Logf, reason LogSupportInfoReason) { + var b strings.Builder + if err := getSupportInfo(&b, reason); err != nil { + logf("error encoding support info: %v", err) + return + } + logf("%s", b.String()) +} + +const ( + supportInfoKeyModules = "modules" + supportInfoKeyRegistry = "registry" +) + +func getSupportInfo(w io.Writer, reason LogSupportInfoReason) error { + output := make(map[string]any) + + regInfo, err := getRegistrySupportInfo(registry.LOCAL_MACHINE, []string{`SOFTWARE\Policies\Tailscale`, winutil.RegBase}) + if err == nil { + output[supportInfoKeyRegistry] = regInfo + } else { + output[supportInfoKeyRegistry] = err + } + + if reason == LogSupportInfoReasonBugReport { + modInfo, err := getModuleInfo() + if err == nil { + output[supportInfoKeyModules] = modInfo + } else { + output[supportInfoKeyModules] = err + } + } + + enc := json.NewEncoder(w) + return enc.Encode(output) +} + +type getRegistrySupportInfoBufs struct { + nameBuf []uint16 + valueBuf []byte +} + +func getRegistrySupportInfo(root registry.Key, subKeys []string) (map[string]any, error) { + bufs := getRegistrySupportInfoBufs{ + nameBuf: make([]uint16, maxRegValueNameLen), + valueBuf: make([]byte, initialValueBufLen), + } + + output := make(map[string]any) + + for _, subKey := range subKeys { + if err := getRegSubKey(root, subKey, 5, &bufs, output); err != nil && !errors.Is(err, registry.ErrNotExist) { + return nil, fmt.Errorf("getRegistrySupportInfo: %w", err) + } + } + + return output, nil +} + +func keyString(key registry.Key, subKey string) string { + var keyStr string + switch key { + case registry.CLASSES_ROOT: + keyStr = `HKCR\` + case registry.CURRENT_USER: + keyStr = `HKCU\` + case registry.LOCAL_MACHINE: + keyStr = `HKLM\` + case registry.USERS: + keyStr = `HKU\` + case registry.CURRENT_CONFIG: + keyStr = `HKCC\` + case registry.PERFORMANCE_DATA: + keyStr = `HKPD\` + default: + } + + return keyStr + subKey +} + +func getRegSubKey(key registry.Key, subKey string, recursionLimit int, bufs *getRegistrySupportInfoBufs, output map[string]any) error { + keyStr := keyString(key, subKey) + k, err := registry.OpenKey(key, subKey, registry.READ) + if err != nil { + return fmt.Errorf("opening %q: %w", keyStr, err) + } + defer k.Close() + + kv := make(map[string]any) + index := uint32(0) + +loopValues: + for { + nbuf := bufs.nameBuf + nameLen := uint32(len(nbuf)) + valueType := uint32(0) + vbuf := bufs.valueBuf + valueLen := uint32(len(vbuf)) + + err := regEnumValue(k, index, &nbuf[0], &nameLen, nil, &valueType, &vbuf[0], &valueLen) + switch err { + case windows.ERROR_NO_MORE_ITEMS: + break loopValues + case windows.ERROR_MORE_DATA: + bufs.valueBuf = make([]byte, valueLen) + continue + case nil: + default: + return fmt.Errorf("regEnumValue: %w", err) + } + + var value any + + switch valueType { + case registry.SZ, registry.EXPAND_SZ: + value = windows.UTF16PtrToString((*uint16)(unsafe.Pointer(&vbuf[0]))) + case registry.BINARY: + if valueLen > maxBinaryValueLen { + valueLen = maxBinaryValueLen + } + value = append([]byte{}, vbuf[:valueLen]...) + case registry.DWORD: + value = binary.LittleEndian.Uint32(vbuf[:4]) + case registry.MULTI_SZ: + // Adapted from x/sys/windows/registry/(Key).GetStringsValue + p := (*[1 << 29]uint16)(unsafe.Pointer(&vbuf[0]))[: valueLen/2 : valueLen/2] + var strs []string + if len(p) > 0 { + if p[len(p)-1] == 0 { + p = p[:len(p)-1] + } + strs = make([]string, 0, 5) + from := 0 + for i, c := range p { + if c == 0 { + strs = append(strs, string(utf16.Decode(p[from:i]))) + from = i + 1 + } + } + } + value = strs + case registry.QWORD: + value = binary.LittleEndian.Uint64(vbuf[:8]) + default: + value = fmt.Sprintf("", valueType) + } + + kv[windows.UTF16PtrToString(&nbuf[0])] = value + index++ + } + + if recursionLimit > 0 { + if sks, err := k.ReadSubKeyNames(0); err == nil { + for _, sk := range sks { + if err := getRegSubKey(k, sk, recursionLimit-1, bufs, kv); err != nil { + return err + } + } + } + } + + output[keyStr] = kv + return nil +} + +type moduleInfo struct { + path string `json:"-"` // internal use only + BaseAddress uintptr `json:"baseAddress"` + Size uint32 `json:"size"` + DebugInfo map[string]string `json:"debugInfo,omitempty"` // map for JSON marshaling purposes + DebugInfoErr error `json:"debugInfoErr,omitempty"` + Signature map[string]string `json:"signature,omitempty"` // map for JSON marshaling purposes + SignatureErr error `json:"signatureErr,omitempty"` + VersionInfo map[string]string `json:"versionInfo,omitempty"` // map for JSON marshaling purposes + VersionErr error `json:"versionErr,omitempty"` +} + +func (mi *moduleInfo) setVersionInfo() { + vi, err := pe.NewVersionInfo(mi.path) + if err != nil { + if !errors.Is(err, pe.ErrNotPresent) { + mi.VersionErr = err + } + return + } + + info := map[string]string{ + "": vi.VersionNumber().String(), + } + + ci, err := vi.Field("CompanyName") + if err == nil { + info["companyName"] = ci + } + + mi.VersionInfo = info +} + +var errAssertingType = errors.New("asserting DataDirectory type") + +func (mi *moduleInfo) setDebugInfo(base uintptr, size uint32) { + pem, err := pe.NewPEFromBaseAddressAndSize(base, size) + if err != nil { + mi.DebugInfoErr = err + return + } + defer pem.Close() + + debugDirAny, err := pem.DataDirectoryEntry(pe.IMAGE_DIRECTORY_ENTRY_DEBUG) + if err != nil { + if !errors.Is(err, pe.ErrNotPresent) { + mi.DebugInfoErr = err + } + return + } + + debugDir, ok := debugDirAny.([]pe.IMAGE_DEBUG_DIRECTORY) + if !ok { + mi.DebugInfoErr = errAssertingType + return + } + + for _, dde := range debugDir { + if dde.Type != pe.IMAGE_DEBUG_TYPE_CODEVIEW { + continue + } + + cv, err := pem.ExtractCodeViewInfo(dde) + if err == nil { + mi.DebugInfo = map[string]string{ + "id": cv.String(), + "pdb": strings.ToLower(filepath.Base(cv.PDBPath)), + } + } else { + mi.DebugInfoErr = err + } + + return + } +} + +func (mi *moduleInfo) setAuthenticodeInfo() { + certSubject, provenance, err := authenticode.QueryCertSubject(mi.path) + if err != nil { + if !errors.Is(err, authenticode.ErrSigNotFound) { + mi.SignatureErr = err + } + return + } + + sigInfo := map[string]string{ + "subject": certSubject, + } + + switch provenance { + case authenticode.SigProvEmbedded: + sigInfo["provenance"] = "embedded" + case authenticode.SigProvCatalog: + sigInfo["provenance"] = "catalog" + default: + } + + mi.Signature = sigInfo +} + +func getModuleInfo() (map[string]moduleInfo, error) { + // Take a snapshot of all modules currently loaded into the current process + snap, err := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPMODULE, 0) + if err != nil { + return nil, err + } + defer windows.CloseHandle(snap) + + result := make(map[string]moduleInfo) + me := windows.ModuleEntry32{ + Size: uint32(unsafe.Sizeof(windows.ModuleEntry32{})), + } + + // Now walk the list + for merr := windows.Module32First(snap, &me); merr == nil; merr = windows.Module32Next(snap, &me) { + name := strings.ToLower(windows.UTF16ToString(me.Module[:])) + path := windows.UTF16ToString(me.ExePath[:]) + base := me.ModBaseAddr + size := me.ModBaseSize + + entry := moduleInfo{ + path: path, + BaseAddress: base, + Size: size, + } + + entry.setVersionInfo() + entry.setDebugInfo(base, size) + entry.setAuthenticodeInfo() + + result[name] = entry + } + + return result, nil +} diff --git a/util/osdiag/osdiag_windows_test.go b/util/osdiag/osdiag_windows_test.go new file mode 100644 index 000000000..61a76155f --- /dev/null +++ b/util/osdiag/osdiag_windows_test.go @@ -0,0 +1,128 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +package osdiag + +import ( + "errors" + "fmt" + "strings" + "testing" + + "golang.org/x/exp/maps" + "golang.org/x/sys/windows/registry" +) + +func makeLongBinaryValue() []byte { + buf := make([]byte, maxBinaryValueLen*2) + for i, _ := range buf { + buf[i] = byte(i % 0xFF) + } + return buf +} + +var testData = map[string]any{ + "": "I am the default", + "StringEmpty": "", + "StringShort": "Hello", + "StringLong": strings.Repeat("7", initialValueBufLen+1), + "MultiStringEmpty": []string{}, + "MultiStringSingle": []string{"Foo"}, + "MultiStringSingleEmpty": []string{""}, + "MultiString": []string{"Foo", "Bar", "Baz"}, + "MultiStringWithEmptyBeginning": []string{"", "Foo", "Bar"}, + "MultiStringWithEmptyMiddle": []string{"Foo", "", "Bar"}, + "MultiStringWithEmptyEnd": []string{"Foo", "Bar", ""}, + "DWord": uint32(0x12345678), + "QWord": uint64(0x123456789abcdef0), + "BinaryEmpty": []byte{}, + "BinaryShort": []byte{0x01, 0x02, 0x03, 0x04}, + "BinaryLong": makeLongBinaryValue(), +} + +const ( + keyNameTest = `SOFTWARE\Tailscale Test` + subKeyNameTest = "SubKey" +) + +func setValues(t *testing.T, k registry.Key) { + for vk, v := range testData { + var err error + switch tv := v.(type) { + case string: + err = k.SetStringValue(vk, tv) + case []string: + err = k.SetStringsValue(vk, tv) + case uint32: + err = k.SetDWordValue(vk, tv) + case uint64: + err = k.SetQWordValue(vk, tv) + case []byte: + err = k.SetBinaryValue(vk, tv) + default: + t.Fatalf("Unknown type") + } + + if err != nil { + t.Fatalf("Error setting %q: %v", vk, err) + } + } +} + +func TestRegistrySupportInfo(t *testing.T) { + // Make sure the key doesn't exist yet + k, err := registry.OpenKey(registry.CURRENT_USER, keyNameTest, registry.READ) + switch { + case err == nil: + k.Close() + t.Fatalf("Test key already exists") + case !errors.Is(err, registry.ErrNotExist): + t.Fatal(err) + } + + func() { + k, _, err := registry.CreateKey(registry.CURRENT_USER, keyNameTest, registry.WRITE) + if err != nil { + t.Fatalf("Error creating test key: %v", err) + } + defer k.Close() + + setValues(t, k) + + sk, _, err := registry.CreateKey(k, subKeyNameTest, registry.WRITE) + if err != nil { + t.Fatalf("Error creating test subkey: %v", err) + } + defer sk.Close() + + setValues(t, sk) + }() + + t.Cleanup(func() { + registry.DeleteKey(registry.CURRENT_USER, keyNameTest+"\\"+subKeyNameTest) + registry.DeleteKey(registry.CURRENT_USER, keyNameTest) + }) + + wantValuesData := maps.Clone(testData) + wantValuesData["BinaryLong"] = (wantValuesData["BinaryLong"].([]byte))[:maxBinaryValueLen] + + wantKeyData := make(map[string]any) + maps.Copy(wantKeyData, wantValuesData) + wantSubKeyData := make(map[string]any) + maps.Copy(wantSubKeyData, wantValuesData) + wantKeyData[subKeyNameTest] = wantSubKeyData + + wantData := map[string]any{ + "HKCU\\" + keyNameTest: wantKeyData, + } + + gotData, err := getRegistrySupportInfo(registry.CURRENT_USER, []string{keyNameTest}) + if err != nil { + t.Errorf("getRegistrySupportInfo error: %v", err) + } + + want, got := fmt.Sprintf("%#v", wantData), fmt.Sprintf("%#v", gotData) + if want != got { + t.Errorf("Compare error: want\n%s,\ngot %s", want, got) + } +} diff --git a/util/osdiag/zsyscall_windows.go b/util/osdiag/zsyscall_windows.go new file mode 100644 index 000000000..caeb245d2 --- /dev/null +++ b/util/osdiag/zsyscall_windows.go @@ -0,0 +1,53 @@ +// Code generated by 'go generate'; DO NOT EDIT. + +package osdiag + +import ( + "syscall" + "unsafe" + + "golang.org/x/sys/windows" + "golang.org/x/sys/windows/registry" +) + +var _ unsafe.Pointer + +// Do the interface allocations only once for common +// Errno values. +const ( + errnoERROR_IO_PENDING = 997 +) + +var ( + errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING) + errERROR_EINVAL error = syscall.EINVAL +) + +// errnoErr returns common boxed Errno values, to prevent +// allocations at runtime. +func errnoErr(e syscall.Errno) error { + switch e { + case 0: + return errERROR_EINVAL + case errnoERROR_IO_PENDING: + return errERROR_IO_PENDING + } + // TODO: add more here, after collecting data on the common + // error values see on Windows. (perhaps when running + // all.bat?) + return e +} + +var ( + modadvapi32 = windows.NewLazySystemDLL("advapi32.dll") + + procRegEnumValueW = modadvapi32.NewProc("RegEnumValueW") +) + +func regEnumValue(key registry.Key, index uint32, valueName *uint16, valueNameLen *uint32, reserved *uint32, valueType *uint32, pData *byte, cbData *uint32) (ret error) { + r0, _, _ := syscall.Syscall9(procRegEnumValueW.Addr(), 8, uintptr(key), uintptr(index), uintptr(unsafe.Pointer(valueName)), uintptr(unsafe.Pointer(valueNameLen)), uintptr(unsafe.Pointer(reserved)), uintptr(unsafe.Pointer(valueType)), uintptr(unsafe.Pointer(pData)), uintptr(unsafe.Pointer(cbData)), 0) + if r0 != 0 { + ret = syscall.Errno(r0) + } + return +} diff --git a/util/winutil/authenticode/authenticode_windows.go b/util/winutil/authenticode/authenticode_windows.go index 4393da478..88737deca 100644 --- a/util/winutil/authenticode/authenticode_windows.go +++ b/util/winutil/authenticode/authenticode_windows.go @@ -260,6 +260,9 @@ func extractCertBlob(hfile windows.Handle) ([]byte, error) { certsAny, err := pef.DataDirectoryEntry(pe.IMAGE_DIRECTORY_ENTRY_SECURITY) if err != nil { + if errors.Is(err, pe.ErrNotPresent) { + err = ErrSigNotFound + } return nil, err } diff --git a/util/winutil/mksyscall.go b/util/winutil/mksyscall.go index f54c3273d..c0a9b2082 100644 --- a/util/winutil/mksyscall.go +++ b/util/winutil/mksyscall.go @@ -7,4 +7,3 @@ package winutil //go:generate go run golang.org/x/tools/cmd/goimports -w zsyscall_windows.go //sys queryServiceConfig2(hService windows.Handle, infoLevel uint32, buf *byte, bufLen uint32, bytesNeeded *uint32) (err error) [failretval==0] = advapi32.QueryServiceConfig2W -//sys regEnumValue(key registry.Key, index uint32, valueName *uint16, valueNameLen *uint32, reserved *uint32, valueType *uint32, pData *byte, cbData *uint32) (ret error) [failretval!=0] = advapi32.RegEnumValueW diff --git a/util/winutil/winutil_windows.go b/util/winutil/winutil_windows.go index 1b2eff00f..89fc543db 100644 --- a/util/winutil/winutil_windows.go +++ b/util/winutil/winutil_windows.go @@ -4,11 +4,8 @@ package winutil import ( - "encoding/binary" - "encoding/json" "errors" "fmt" - "io" "log" "os/exec" "os/user" @@ -16,12 +13,10 @@ import ( "strings" "syscall" "time" - "unicode/utf16" "unsafe" "golang.org/x/sys/windows" "golang.org/x/sys/windows/registry" - "tailscale.com/types/logger" ) const ( @@ -556,166 +551,3 @@ func findHomeDirInRegistry(uid string) (dir string, err error) { } return dir, nil } - -const ( - maxBinaryValueLen = 128 // we'll truncate any binary values longer than this - maxRegValueNameLen = 16384 // maximum length supported by Windows + 1 - initialValueBufLen = 80 // large enough to contain a stringified GUID encoded as UTF-16 -) - -const ( - supportInfoKeyRegistry = "Registry" -) - -// LogSupportInfo obtains information useful for troubleshooting and support, -// and writes it to the log as a JSON-encoded object. -func LogSupportInfo(logf logger.Logf) { - var b strings.Builder - if err := getSupportInfo(&b); err != nil { - log.Printf("error encoding support info: %v", err) - return - } - logf("Support Info: %s", b.String()) -} - -func getSupportInfo(w io.Writer) error { - output := make(map[string]any) - - regInfo, err := getRegistrySupportInfo(registry.LOCAL_MACHINE, []string{regPolicyBase, regBase}) - if err == nil { - output[supportInfoKeyRegistry] = regInfo - } else { - output[supportInfoKeyRegistry] = err - } - - enc := json.NewEncoder(w) - return enc.Encode(output) -} - -type getRegistrySupportInfoBufs struct { - nameBuf []uint16 - valueBuf []byte -} - -func getRegistrySupportInfo(root registry.Key, subKeys []string) (map[string]any, error) { - bufs := getRegistrySupportInfoBufs{ - nameBuf: make([]uint16, maxRegValueNameLen), - valueBuf: make([]byte, initialValueBufLen), - } - - output := make(map[string]any) - - for _, subKey := range subKeys { - if err := getRegSubKey(root, subKey, 5, &bufs, output); err != nil && !errors.Is(err, registry.ErrNotExist) { - return nil, fmt.Errorf("getRegistrySupportInfo: %w", err) - } - } - - return output, nil -} - -func keyString(key registry.Key, subKey string) string { - var keyStr string - switch key { - case registry.CLASSES_ROOT: - keyStr = `HKCR\` - case registry.CURRENT_USER: - keyStr = `HKCU\` - case registry.LOCAL_MACHINE: - keyStr = `HKLM\` - case registry.USERS: - keyStr = `HKU\` - case registry.CURRENT_CONFIG: - keyStr = `HKCC\` - case registry.PERFORMANCE_DATA: - keyStr = `HKPD\` - default: - } - - return keyStr + subKey -} - -func getRegSubKey(key registry.Key, subKey string, recursionLimit int, bufs *getRegistrySupportInfoBufs, output map[string]any) error { - keyStr := keyString(key, subKey) - k, err := registry.OpenKey(key, subKey, registry.READ) - if err != nil { - return fmt.Errorf("opening %q: %w", keyStr, err) - } - defer k.Close() - - kv := make(map[string]any) - index := uint32(0) - -loopValues: - for { - nbuf := bufs.nameBuf - nameLen := uint32(len(nbuf)) - valueType := uint32(0) - vbuf := bufs.valueBuf - valueLen := uint32(len(vbuf)) - - err := regEnumValue(k, index, &nbuf[0], &nameLen, nil, &valueType, &vbuf[0], &valueLen) - switch err { - case windows.ERROR_NO_MORE_ITEMS: - break loopValues - case windows.ERROR_MORE_DATA: - bufs.valueBuf = make([]byte, valueLen) - continue - case nil: - default: - return fmt.Errorf("regEnumValue: %w", err) - } - - var value any - - switch valueType { - case registry.SZ, registry.EXPAND_SZ: - value = windows.UTF16PtrToString((*uint16)(unsafe.Pointer(&vbuf[0]))) - case registry.BINARY: - if valueLen > maxBinaryValueLen { - valueLen = maxBinaryValueLen - } - value = append([]byte{}, vbuf[:valueLen]...) - case registry.DWORD: - value = binary.LittleEndian.Uint32(vbuf[:4]) - case registry.MULTI_SZ: - // Adapted from x/sys/windows/registry/(Key).GetStringsValue - p := (*[1 << 29]uint16)(unsafe.Pointer(&vbuf[0]))[: valueLen/2 : valueLen/2] - var strs []string - if len(p) > 0 { - if p[len(p)-1] == 0 { - p = p[:len(p)-1] - } - strs = make([]string, 0, 5) - from := 0 - for i, c := range p { - if c == 0 { - strs = append(strs, string(utf16.Decode(p[from:i]))) - from = i + 1 - } - } - } - value = strs - case registry.QWORD: - value = binary.LittleEndian.Uint64(vbuf[:8]) - default: - value = fmt.Sprintf("", valueType) - } - - kv[windows.UTF16PtrToString(&nbuf[0])] = value - index++ - } - - if recursionLimit > 0 { - if sks, err := k.ReadSubKeyNames(0); err == nil { - for _, sk := range sks { - if err := getRegSubKey(k, sk, recursionLimit-1, bufs, kv); err != nil { - return err - } - } - } - } - - output[keyStr] = kv - return nil -} diff --git a/util/winutil/winutil_windows_test.go b/util/winutil/winutil_windows_test.go index e9ca08b09..bf22d26ca 100644 --- a/util/winutil/winutil_windows_test.go +++ b/util/winutil/winutil_windows_test.go @@ -4,13 +4,7 @@ package winutil import ( - "errors" - "fmt" - "strings" "testing" - - "golang.org/x/exp/maps" - "golang.org/x/sys/windows/registry" ) const ( @@ -34,117 +28,3 @@ func TestLookupPseudoUser(t *testing.T) { t.Errorf("LookupPseudoUser(%q) unexpectedly succeeded", networkSID) } } - -func makeLongBinaryValue() []byte { - buf := make([]byte, maxBinaryValueLen*2) - for i, _ := range buf { - buf[i] = byte(i % 0xFF) - } - return buf -} - -var testData = map[string]any{ - "": "I am the default", - "StringEmpty": "", - "StringShort": "Hello", - "StringLong": strings.Repeat("7", initialValueBufLen+1), - "MultiStringEmpty": []string{}, - "MultiStringSingle": []string{"Foo"}, - "MultiStringSingleEmpty": []string{""}, - "MultiString": []string{"Foo", "Bar", "Baz"}, - "MultiStringWithEmptyBeginning": []string{"", "Foo", "Bar"}, - "MultiStringWithEmptyMiddle": []string{"Foo", "", "Bar"}, - "MultiStringWithEmptyEnd": []string{"Foo", "Bar", ""}, - "DWord": uint32(0x12345678), - "QWord": uint64(0x123456789abcdef0), - "BinaryEmpty": []byte{}, - "BinaryShort": []byte{0x01, 0x02, 0x03, 0x04}, - "BinaryLong": makeLongBinaryValue(), -} - -const ( - keyNameTest = `SOFTWARE\Tailscale Test` - subKeyNameTest = "SubKey" -) - -func setValues(t *testing.T, k registry.Key) { - for vk, v := range testData { - var err error - switch tv := v.(type) { - case string: - err = k.SetStringValue(vk, tv) - case []string: - err = k.SetStringsValue(vk, tv) - case uint32: - err = k.SetDWordValue(vk, tv) - case uint64: - err = k.SetQWordValue(vk, tv) - case []byte: - err = k.SetBinaryValue(vk, tv) - default: - t.Fatalf("Unknown type") - } - - if err != nil { - t.Fatalf("Error setting %q: %v", vk, err) - } - } -} - -func TestRegistrySupportInfo(t *testing.T) { - // Make sure the key doesn't exist yet - k, err := registry.OpenKey(registry.CURRENT_USER, keyNameTest, registry.READ) - switch { - case err == nil: - k.Close() - t.Fatalf("Test key already exists") - case !errors.Is(err, registry.ErrNotExist): - t.Fatal(err) - } - - func() { - k, _, err := registry.CreateKey(registry.CURRENT_USER, keyNameTest, registry.WRITE) - if err != nil { - t.Fatalf("Error creating test key: %v", err) - } - defer k.Close() - - setValues(t, k) - - sk, _, err := registry.CreateKey(k, subKeyNameTest, registry.WRITE) - if err != nil { - t.Fatalf("Error creating test subkey: %v", err) - } - defer sk.Close() - - setValues(t, sk) - }() - - t.Cleanup(func() { - registry.DeleteKey(registry.CURRENT_USER, keyNameTest+"\\"+subKeyNameTest) - registry.DeleteKey(registry.CURRENT_USER, keyNameTest) - }) - - wantValuesData := maps.Clone(testData) - wantValuesData["BinaryLong"] = (wantValuesData["BinaryLong"].([]byte))[:maxBinaryValueLen] - - wantKeyData := make(map[string]any) - maps.Copy(wantKeyData, wantValuesData) - wantSubKeyData := make(map[string]any) - maps.Copy(wantSubKeyData, wantValuesData) - wantKeyData[subKeyNameTest] = wantSubKeyData - - wantData := map[string]any{ - "HKCU\\" + keyNameTest: wantKeyData, - } - - gotData, err := getRegistrySupportInfo(registry.CURRENT_USER, []string{keyNameTest}) - if err != nil { - t.Errorf("getRegistrySupportInfo error: %v", err) - } - - want, got := fmt.Sprintf("%#v", wantData), fmt.Sprintf("%#v", gotData) - if want != got { - t.Errorf("Compare error: want\n%s,\ngot %s", want, got) - } -} diff --git a/util/winutil/zsyscall_windows.go b/util/winutil/zsyscall_windows.go index 930a87522..8c899232f 100644 --- a/util/winutil/zsyscall_windows.go +++ b/util/winutil/zsyscall_windows.go @@ -7,7 +7,6 @@ import ( "unsafe" "golang.org/x/sys/windows" - "golang.org/x/sys/windows/registry" ) var _ unsafe.Pointer @@ -42,7 +41,6 @@ var ( modadvapi32 = windows.NewLazySystemDLL("advapi32.dll") procQueryServiceConfig2W = modadvapi32.NewProc("QueryServiceConfig2W") - procRegEnumValueW = modadvapi32.NewProc("RegEnumValueW") ) func queryServiceConfig2(hService windows.Handle, infoLevel uint32, buf *byte, bufLen uint32, bytesNeeded *uint32) (err error) { @@ -52,11 +50,3 @@ func queryServiceConfig2(hService windows.Handle, infoLevel uint32, buf *byte, b } return } - -func regEnumValue(key registry.Key, index uint32, valueName *uint16, valueNameLen *uint32, reserved *uint32, valueType *uint32, pData *byte, cbData *uint32) (ret error) { - r0, _, _ := syscall.Syscall9(procRegEnumValueW.Addr(), 8, uintptr(key), uintptr(index), uintptr(unsafe.Pointer(valueName)), uintptr(unsafe.Pointer(valueNameLen)), uintptr(unsafe.Pointer(reserved)), uintptr(unsafe.Pointer(valueType)), uintptr(unsafe.Pointer(pData)), uintptr(unsafe.Pointer(cbData)), 0) - if r0 != 0 { - ret = syscall.Errno(r0) - } - return -}