// Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause package osdiag import ( "encoding/binary" "errors" "fmt" "path/filepath" "strings" "unicode/utf16" "unsafe" "github.com/dblohm7/wingoes/com" "github.com/dblohm7/wingoes/pe" "golang.org/x/sys/windows" "golang.org/x/sys/windows/registry" "tailscale.com/util/osdiag/internal/wsc" "tailscale.com/util/winutil" "tailscale.com/util/winutil/authenticode" ) var ( errUnexpectedResult = errors.New("API call returned an unexpected value") ) 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 ( supportInfoKeyModules = "modules" supportInfoKeyPageFile = "pageFile" supportInfoKeyRegistry = "registry" supportInfoKeySecurity = "securitySoftware" supportInfoKeyWinsockLSP = "winsockLSP" ) func supportInfo(reason LogSupportInfoReason) map[string]any { output := make(map[string]any) regInfo, err := getRegistrySupportInfo(registry.LOCAL_MACHINE, []string{winutil.RegPolicyBase, winutil.RegBase}) if err == nil { output[supportInfoKeyRegistry] = regInfo } else { output[supportInfoKeyRegistry] = err } pageFileInfo, err := getPageFileInfo() if err == nil { output[supportInfoKeyPageFile] = pageFileInfo } else { output[supportInfoKeyPageFile] = err } if reason == LogSupportInfoReasonBugReport { modInfo, err := getModuleInfo() if err == nil { output[supportInfoKeyModules] = modInfo } else { output[supportInfoKeyModules] = err } output[supportInfoKeySecurity] = getSecurityInfo() lspInfo, err := getWinsockLSPInfo() if err == nil { output[supportInfoKeyWinsockLSP] = lspInfo } else { output[supportInfoKeyWinsockLSP] = err } } return 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() { pem, err := pe.NewPEFromBaseAddressAndSize(mi.BaseAddress, mi.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() entry.setAuthenticodeInfo() result[name] = entry } return result, nil } type _WSC_PROVIDER_INFO_TYPE int32 const ( providerInfoLspCategories _WSC_PROVIDER_INFO_TYPE = 0 ) const ( _SOCKET_ERROR = -1 ) // Note that wsaProtocolInfo needs to be identical to windows.WSAProtocolInfo; // the purpose of this type is to have the ability to use it as a reciever in // the path and categoryFlags funcs defined below. type wsaProtocolInfo windows.WSAProtocolInfo func (pi *wsaProtocolInfo) path() (string, error) { var errno int32 var buf [windows.MAX_PATH]uint16 bufCount := int32(len(buf)) ret := wscGetProviderPath(&pi.ProviderId, &buf[0], &bufCount, &errno) if ret == _SOCKET_ERROR { return "", windows.Errno(errno) } if ret != 0 { return "", errUnexpectedResult } return windows.UTF16ToString(buf[:bufCount]), nil } func (pi *wsaProtocolInfo) categoryFlags() (uint32, error) { var errno int32 var result uint32 bufLen := uintptr(unsafe.Sizeof(result)) ret := wscGetProviderInfo(&pi.ProviderId, providerInfoLspCategories, unsafe.Pointer(&result), &bufLen, 0, &errno) if ret == _SOCKET_ERROR { return 0, windows.Errno(errno) } if ret != 0 { return 0, errUnexpectedResult } return result, nil } type wsaProtocolInfoOutput struct { Description string `json:"description,omitempty"` Version int32 `json:"version"` AddressFamily int32 `json:"addressFamily"` SocketType int32 `json:"socketType"` Protocol int32 `json:"protocol"` ServiceFlags1 string `json:"serviceFlags1"` ProviderFlags string `json:"providerFlags"` Path string `json:"path,omitempty"` PathErr error `json:"pathErr,omitempty"` Category string `json:"category,omitempty"` CategoryErr error `json:"categoryErr,omitempty"` BaseProviderID string `json:"baseProviderID,omitempty"` LayerProviderID string `json:"layerProviderID,omitempty"` Chain []uint32 `json:"chain,omitempty"` } func getWinsockLSPInfo() (map[uint32]wsaProtocolInfoOutput, error) { protocols, err := enumWinsockProtocols() if err != nil { return nil, err } result := make(map[uint32]wsaProtocolInfoOutput, len(protocols)) for _, p := range protocols { v := wsaProtocolInfoOutput{ Description: windows.UTF16ToString(p.ProtocolName[:]), Version: p.Version, AddressFamily: p.AddressFamily, SocketType: p.SocketType, Protocol: p.Protocol, ServiceFlags1: fmt.Sprintf("0x%08X", p.ServiceFlags1), // Serializing as hex string to make the flags easier to decode by human inspection ProviderFlags: fmt.Sprintf("0x%08X", p.ProviderFlags), } switch p.ProtocolChain.ChainLen { case windows.BASE_PROTOCOL: v.BaseProviderID = p.ProviderId.String() case windows.LAYERED_PROTOCOL: v.LayerProviderID = p.ProviderId.String() default: v.Chain = p.ProtocolChain.ChainEntries[:p.ProtocolChain.ChainLen] } // Queries that are only valid for base and layered protocols (not chains) if v.Chain == nil { path, err := p.path() if err == nil { v.Path = strings.ToLower(path) } else { v.PathErr = err } category, err := p.categoryFlags() if err == nil { v.Category = fmt.Sprintf("0x%08X", category) } else if !errors.Is(err, windows.WSAEINVALIDPROVIDER) { // WSAEINVALIDPROVIDER == "no category info found", so we only log // errors other than that one. v.CategoryErr = err } } // Chains reference other providers using catalog entry IDs, so we use that // value as the key in our map. result[p.CatalogEntryId] = v } return result, nil } func enumWinsockProtocols() ([]wsaProtocolInfo, error) { // Get the required size var errno int32 var bytesReqd uint32 ret := wscEnumProtocols(nil, nil, &bytesReqd, &errno) if ret != _SOCKET_ERROR { return nil, errUnexpectedResult } if e := windows.Errno(errno); e != windows.WSAENOBUFS { return nil, e } // Allocate szEntry := uint32(unsafe.Sizeof(wsaProtocolInfo{})) buf := make([]wsaProtocolInfo, bytesReqd/szEntry) // Now do the query for real bufLen := uint32(len(buf)) * szEntry ret = wscEnumProtocols(nil, &buf[0], &bufLen, &errno) if ret == _SOCKET_ERROR { return nil, windows.Errno(errno) } return buf, nil } type providerKey struct { provType wsc.WSC_SECURITY_PROVIDER provKey string } var providerKeys = []providerKey{ providerKey{ wsc.WSC_SECURITY_PROVIDER_ANTIVIRUS, "av", }, providerKey{ wsc.WSC_SECURITY_PROVIDER_ANTISPYWARE, "antispy", }, providerKey{ wsc.WSC_SECURITY_PROVIDER_FIREWALL, "firewall", }, } const ( maxProvCount = 100 ) type secProductInfo struct { Name string `json:"name,omitempty"` NameErr error `json:"nameErr,omitempty"` State string `json:"state,omitempty"` StateErr error `json:"stateErr,omitempty"` } func getSecurityInfo() map[string]any { result := make(map[string]any) for _, prov := range providerKeys { // Note that we need to obtain a new product list for each provider type; // the docs clearly state that we cannot reuse objects. productList, err := com.CreateInstance[wsc.WSCProductList](wsc.CLSID_WSCProductList) if err != nil { result[prov.provKey] = err continue } err = productList.Initialize(prov.provType) if err != nil { result[prov.provKey] = err continue } n, err := productList.GetCount() if err != nil { result[prov.provKey] = err continue } if n == 0 { continue } n = min(n, maxProvCount) values := make([]any, 0, n) for i := int32(0); i < n; i++ { product, err := productList.GetItem(uint32(i)) if err != nil { values = append(values, err) continue } var value secProductInfo value.Name, err = product.GetProductName() if err != nil { value.NameErr = err } state, err := product.GetProductState() if err == nil { switch state { case wsc.WSC_SECURITY_PRODUCT_STATE_ON: value.State = "on" case wsc.WSC_SECURITY_PRODUCT_STATE_OFF: value.State = "off" case wsc.WSC_SECURITY_PRODUCT_STATE_SNOOZED: value.State = "snoozed" case wsc.WSC_SECURITY_PRODUCT_STATE_EXPIRED: value.State = "expired" default: value.State = fmt.Sprintf("", state) } } else { value.StateErr = err } values = append(values, value) } result[prov.provKey] = values } return result } type _MEMORYSTATUSEX struct { Length uint32 MemoryLoad uint32 TotalPhys uint64 AvailPhys uint64 TotalPageFile uint64 AvailPageFile uint64 TotalVirtual uint64 AvailVirtual uint64 AvailExtendedVirtual uint64 } func getPageFileInfo() (map[string]any, error) { memStatus := _MEMORYSTATUSEX{ Length: uint32(unsafe.Sizeof(_MEMORYSTATUSEX{})), } if err := globalMemoryStatusEx(&memStatus); err != nil { return nil, err } result := map[string]any{ "bytesAvailable": memStatus.AvailPageFile, "bytesTotal": memStatus.TotalPageFile, } if entries, err := getEffectivePageFileValue(); err == nil { // autoManaged is set to true when there is at least one page file that // is automatically managed. autoManaged := false // If there is only one entry that consists of only one part, then // the page files are 100% managed by the system. // If there are multiple entries, then each one must be checked. // Each entry then consists of three components, deliminated by spaces. // If the latter two components are both "0", then that entry is auto-managed. for _, entry := range entries { if parts := strings.Split(entry, " "); (len(parts) == 1 && len(entries) == 1) || (len(parts) == 3 && parts[1] == "0" && parts[2] == "0") { autoManaged = true break } } result["autoManaged"] = autoManaged } return result, nil } func getEffectivePageFileValue() ([]string, error) { const subKey = `SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management` key, err := registry.OpenKey(registry.LOCAL_MACHINE, subKey, registry.QUERY_VALUE) if err != nil { return nil, err } defer key.Close() // Rare but possible case: the user has updated their page file config but // they haven't yet rebooted for the change to take effect. This is the // current setting that the machine is still operating with. if entries, _, err := key.GetStringsValue("ExistingPageFiles"); err == nil { return entries, nil } // Otherwise we use this value (yes, the above value uses "Page" and this one uses "Paging"). entries, _, err := key.GetStringsValue("PagingFiles") return entries, err }