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 <aaron@tailscale.com>
This commit is contained in:
Aaron Klotz 2023-06-26 11:50:45 -06:00
parent 301e59f398
commit 37925b3e7a
17 changed files with 575 additions and 305 deletions

View File

@ -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

View File

@ -50,6 +50,7 @@
"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 @@ func isWindowsService() bool {
// 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 {

2
go.mod
View File

@ -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

4
go.sum
View File

@ -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=

View File

@ -48,6 +48,7 @@
"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: "))
}

View File

@ -52,6 +52,7 @@
_ "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"

9
util/osdiag/mksyscall.go Normal file
View File

@ -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

23
util/osdiag/osdiag.go Normal file
View File

@ -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)
}

View File

@ -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) {
}

View File

@ -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("<unsupported value type %d>", 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
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -7,4 +7,3 @@
//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

View File

@ -4,11 +4,8 @@
package winutil
import (
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"os/exec"
"os/user"
@ -16,12 +13,10 @@
"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("<unsupported value type %d>", 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
}

View File

@ -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)
}
}

View File

@ -7,7 +7,6 @@
"unsafe"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/registry"
)
var _ unsafe.Pointer
@ -42,7 +41,6 @@ func errnoErr(e syscall.Errno) error {
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
}