src/os/user/lookup_windows.go GO 552 lines View on github.com → Search inside
1// Copyright 2012 The Go Authors. All rights reserved.2// Use of this source code is governed by a BSD-style3// license that can be found in the LICENSE file.45package user67import (8	"errors"9	"fmt"10	"internal/syscall/windows"11	"internal/syscall/windows/registry"12	"runtime"13	"syscall"14	"unsafe"15)1617func isDomainJoined() (bool, error) {18	var domain *uint1619	var status uint3220	err := syscall.NetGetJoinInformation(nil, &domain, &status)21	if err != nil {22		return false, err23	}24	syscall.NetApiBufferFree((*byte)(unsafe.Pointer(domain)))25	return status == syscall.NetSetupDomainName, nil26}2728func lookupFullNameDomain(domainAndUser string) (string, error) {29	return syscall.TranslateAccountName(domainAndUser,30		syscall.NameSamCompatible, syscall.NameDisplay, 50)31}3233func lookupFullNameServer(servername, username string) (string, error) {34	s, e := syscall.UTF16PtrFromString(servername)35	if e != nil {36		return "", e37	}38	u, e := syscall.UTF16PtrFromString(username)39	if e != nil {40		return "", e41	}42	var p *byte43	e = syscall.NetUserGetInfo(s, u, 10, &p)44	if e != nil {45		return "", e46	}47	defer syscall.NetApiBufferFree(p)48	i := (*syscall.UserInfo10)(unsafe.Pointer(p))49	return windows.UTF16PtrToString(i.FullName), nil50}5152func lookupFullName(domain, username, domainAndUser string) (string, error) {53	joined, err := isDomainJoined()54	if err == nil && joined {55		name, err := lookupFullNameDomain(domainAndUser)56		if err == nil {57			return name, nil58		}59	}60	name, err := lookupFullNameServer(domain, username)61	if err == nil {62		return name, nil63	}64	// domain worked neither as a domain nor as a server65	// could be domain server unavailable66	// pretend username is fullname67	return username, nil68}6970// getProfilesDirectory retrieves the path to the root directory71// where user profiles are stored.72func getProfilesDirectory() (string, error) {73	n := uint32(100)74	for {75		b := make([]uint16, n)76		e := windows.GetProfilesDirectory(&b[0], &n)77		if e == nil {78			return syscall.UTF16ToString(b), nil79		}80		if e != syscall.ERROR_INSUFFICIENT_BUFFER {81			return "", e82		}83		if n <= uint32(len(b)) {84			return "", e85		}86	}87}8889func isServiceAccount(sid *syscall.SID) bool {90	if !windows.IsValidSid(sid) {91		// We don't accept SIDs from the public API, so this should never happen.92		// Better be on the safe side and validate anyway.93		return false94	}95	// The following RIDs are considered service user accounts as per96	// https://learn.microsoft.com/en-us/windows/win32/secauthz/well-known-sids and97	// https://learn.microsoft.com/en-us/windows/win32/services/service-user-accounts:98	// - "S-1-5-18": LocalSystem99	// - "S-1-5-19": LocalService100	// - "S-1-5-20": NetworkService101	if windows.GetSidSubAuthorityCount(sid) != windows.SID_REVISION ||102		windows.GetSidIdentifierAuthority(sid) != windows.SECURITY_NT_AUTHORITY {103		return false104	}105	switch windows.GetSidSubAuthority(sid, 0) {106	case windows.SECURITY_LOCAL_SYSTEM_RID,107		windows.SECURITY_LOCAL_SERVICE_RID,108		windows.SECURITY_NETWORK_SERVICE_RID:109		return true110	}111	return false112}113114func isValidUserAccountType(sid *syscall.SID, sidType uint32) bool {115	switch sidType {116	case syscall.SidTypeUser:117		return true118	case syscall.SidTypeWellKnownGroup:119		return isServiceAccount(sid)120	}121	return false122}123124func isValidGroupAccountType(sidType uint32) bool {125	switch sidType {126	case syscall.SidTypeGroup:127		return true128	case syscall.SidTypeWellKnownGroup:129		// Some well-known groups are also considered service accounts,130		// so isValidUserAccountType would return true for them.131		// We have historically allowed them in LookupGroup and LookupGroupId,132		// so don't treat them as invalid here.133		return true134	case syscall.SidTypeAlias:135		// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-samr/7b2aeb27-92fc-41f6-8437-deb65d950921#gt_0387e636-5654-4910-9519-1f8326cf5ec0136		// SidTypeAlias should also be treated as a group type next to SidTypeGroup137		// and SidTypeWellKnownGroup:138		// "alias object -> resource group: A group object..."139		//140		// Tests show that "Administrators" can be considered of type SidTypeAlias.141		return true142	}143	return false144}145146// lookupUsernameAndDomain obtains the username and domain for usid.147func lookupUsernameAndDomain(usid *syscall.SID) (username, domain string, sidType uint32, e error) {148	username, domain, sidType, e = usid.LookupAccount("")149	if e != nil {150		return "", "", 0, e151	}152	if !isValidUserAccountType(usid, sidType) {153		return "", "", 0, fmt.Errorf("user: should be user account type, not %d", sidType)154	}155	return username, domain, sidType, nil156}157158// findHomeDirInRegistry finds the user home path based on the uid.159func findHomeDirInRegistry(uid string) (dir string, e error) {160	k, e := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\`+uid, registry.QUERY_VALUE)161	if e != nil {162		return "", e163	}164	defer k.Close()165	dir, _, e = k.GetStringValue("ProfileImagePath")166	if e != nil {167		return "", e168	}169	return dir, nil170}171172// lookupGroupName accepts the name of a group and retrieves the group SID.173func lookupGroupName(groupname string) (string, error) {174	sid, _, t, e := syscall.LookupSID("", groupname)175	if e != nil {176		if errors.Is(e, windows.ERROR_NONE_MAPPED) {177			return "", fmt.Errorf("%w: %w", UnknownGroupError(groupname), e)178		}179		return "", e180	}181	if !isValidGroupAccountType(t) {182		return "", fmt.Errorf("lookupGroupName: should be group account type, not %d", t)183	}184	return sid.String()185}186187// listGroupsForUsernameAndDomain accepts username and domain and retrieves188// a SID list of the local groups where this user is a member.189func listGroupsForUsernameAndDomain(username, domain string) ([]string, error) {190	// Check if both the domain name and user should be used.191	var query string192	joined, err := isDomainJoined()193	if err == nil && joined && len(domain) != 0 {194		query = domain + `\` + username195	} else {196		query = username197	}198	q, err := syscall.UTF16PtrFromString(query)199	if err != nil {200		return nil, err201	}202	var p0 *byte203	var entriesRead, totalEntries uint32204	// https://learn.microsoft.com/en-us/windows/win32/api/lmaccess/nf-lmaccess-netusergetlocalgroups205	// NetUserGetLocalGroups() would return a list of LocalGroupUserInfo0206	// elements which hold the names of local groups where the user participates.207	// The list does not follow any sorting order.208	err = windows.NetUserGetLocalGroups(nil, q, 0, windows.LG_INCLUDE_INDIRECT, &p0, windows.MAX_PREFERRED_LENGTH, &entriesRead, &totalEntries)209	if err != nil {210		return nil, err211	}212	defer syscall.NetApiBufferFree(p0)213	if entriesRead == 0 {214		return nil, nil215	}216	entries := (*[1024]windows.LocalGroupUserInfo0)(unsafe.Pointer(p0))[:entriesRead:entriesRead]217	var sids []string218	for _, entry := range entries {219		if entry.Name == nil {220			continue221		}222		sid, err := lookupGroupName(windows.UTF16PtrToString(entry.Name))223		if err != nil {224			return nil, err225		}226		sids = append(sids, sid)227	}228	return sids, nil229}230231func newUser(uid, gid, dir, username, domain string) (*User, error) {232	domainAndUser := domain + `\` + username233	name, e := lookupFullName(domain, username, domainAndUser)234	if e != nil {235		return nil, e236	}237	u := &User{238		Uid:      uid,239		Gid:      gid,240		Username: domainAndUser,241		Name:     name,242		HomeDir:  dir,243	}244	return u, nil245}246247var (248	// unused variables (in this implementation)249	// modified during test to exercise code paths in the cgo implementation.250	userBuffer  = 0251	groupBuffer = 0252)253254func current() (*User, error) {255	// Use runAsProcessOwner to ensure that we can access the process token256	// when calling syscall.OpenCurrentProcessToken if the current thread257	// is impersonating a different user. See https://go.dev/issue/68647.258	var usr *User259	err := runAsProcessOwner(func() error {260		t, e := syscall.OpenCurrentProcessToken()261		if e != nil {262			return e263		}264		defer t.Close()265		u, e := t.GetTokenUser()266		if e != nil {267			return e268		}269		pg, e := t.GetTokenPrimaryGroup()270		if e != nil {271			return e272		}273		uid, e := u.User.Sid.String()274		if e != nil {275			return e276		}277		gid, e := pg.PrimaryGroup.String()278		if e != nil {279			return e280		}281		dir, e := t.GetUserProfileDirectory()282		if e != nil {283			return e284		}285		username, e := windows.GetUserName(syscall.NameSamCompatible)286		if e != nil {287			return e288		}289		displayName, e := windows.GetUserName(syscall.NameDisplay)290		if e != nil {291			// Historically, the username is used as fallback292			// when the display name can't be retrieved.293			displayName = username294		}295		usr = &User{296			Uid:      uid,297			Gid:      gid,298			Username: username,299			Name:     displayName,300			HomeDir:  dir,301		}302		return nil303	})304	return usr, err305}306307// runAsProcessOwner runs f in the context of the current process owner,308// that is, removing any impersonation that may be in effect before calling f,309// and restoring the impersonation afterwards.310func runAsProcessOwner(f func() error) error {311	var impersonationRollbackErr error312	runtime.LockOSThread()313	defer func() {314		// If impersonation failed, the thread is running with the wrong token,315		// so it's better to terminate it.316		// This is achieved by not calling runtime.UnlockOSThread.317		if impersonationRollbackErr != nil {318			println("os/user: failed to revert to previous token:", impersonationRollbackErr.Error())319			runtime.Goexit()320		} else {321			runtime.UnlockOSThread()322		}323	}()324	prevToken, isProcessToken, err := getCurrentToken()325	if err != nil {326		return fmt.Errorf("os/user: failed to get current token: %w", err)327	}328	defer prevToken.Close()329	if !isProcessToken {330		if err = windows.RevertToSelf(); err != nil {331			return fmt.Errorf("os/user: failed to revert to self: %w", err)332		}333		defer func() {334			impersonationRollbackErr = windows.ImpersonateLoggedOnUser(prevToken)335		}()336	}337	return f()338}339340// getCurrentToken returns the current thread token, or341// the process token if the thread doesn't have a token.342func getCurrentToken() (t syscall.Token, isProcessToken bool, err error) {343	thread, _ := windows.GetCurrentThread()344	// Need TOKEN_DUPLICATE and TOKEN_IMPERSONATE to use the token in ImpersonateLoggedOnUser.345	err = windows.OpenThreadToken(thread, syscall.TOKEN_QUERY|syscall.TOKEN_DUPLICATE|syscall.TOKEN_IMPERSONATE, true, &t)346	if errors.Is(err, windows.ERROR_NO_TOKEN) {347		// Not impersonating, use the process token.348		isProcessToken = true349		t, err = syscall.OpenCurrentProcessToken()350	}351	return t, isProcessToken, err352}353354// lookupUserPrimaryGroup obtains the primary group SID for a user using this method:355// https://support.microsoft.com/en-us/help/297951/how-to-use-the-primarygroupid-attribute-to-find-the-primary-group-for356// The method follows this formula: domainRID + "-" + primaryGroupRID357func lookupUserPrimaryGroup(username, domain string) (string, error) {358	// get the domain RID359	sid, _, t, e := syscall.LookupSID("", domain)360	if e != nil {361		return "", e362	}363	if t != syscall.SidTypeDomain {364		return "", fmt.Errorf("lookupUserPrimaryGroup: should be domain account type, not %d", t)365	}366	domainRID, e := sid.String()367	if e != nil {368		return "", e369	}370	// If the user has joined a domain use the RID of the default primary group371	// called "Domain Users":372	// https://support.microsoft.com/en-us/help/243330/well-known-security-identifiers-in-windows-operating-systems373	// SID: S-1-5-21domain-513374	//375	// The correct way to obtain the primary group of a domain user is376	// probing the user primaryGroupID attribute in the server Active Directory:377	// https://learn.microsoft.com/en-us/windows/win32/adschema/a-primarygroupid378	//379	// Note that the primary group of domain users should not be modified380	// on Windows for performance reasons, even if it's possible to do that.381	// The .NET Developer's Guide to Directory Services Programming - Page 409382	// https://books.google.bg/books?id=kGApqjobEfsC&lpg=PA410&ots=p7oo-eOQL7&dq=primary%20group%20RID&hl=bg&pg=PA409#v=onepage&q&f=false383	joined, err := isDomainJoined()384	if err == nil && joined {385		return domainRID + "-513", nil386	}387	// For non-domain users call NetUserGetInfo() with level 4, which388	// in this case would not have any network overhead.389	// The primary group should not change from RID 513 here either390	// but the group will be called "None" instead:391	// https://www.adampalmer.me/iodigitalsec/2013/08/10/windows-null-session-enumeration/392	// "Group 'None' (RID: 513)"393	u, e := syscall.UTF16PtrFromString(username)394	if e != nil {395		return "", e396	}397	d, e := syscall.UTF16PtrFromString(domain)398	if e != nil {399		return "", e400	}401	var p *byte402	e = syscall.NetUserGetInfo(d, u, 4, &p)403	if e != nil {404		return "", e405	}406	defer syscall.NetApiBufferFree(p)407	i := (*windows.UserInfo4)(unsafe.Pointer(p))408	return fmt.Sprintf("%s-%d", domainRID, i.PrimaryGroupID), nil409}410411func newUserFromSid(usid *syscall.SID) (*User, error) {412	username, domain, sidType, e := lookupUsernameAndDomain(usid)413	if e != nil {414		return nil, e415	}416	uid, e := usid.String()417	if e != nil {418		return nil, e419	}420	var gid string421	if sidType == syscall.SidTypeWellKnownGroup {422		// The SID does not contain a domain; this function's domain variable has423		// been populated with the SID's identifier authority. This happens with424		// special service user accounts such as "NT AUTHORITY\LocalSystem".425		// In this case, gid is the same as the user SID.426		gid = uid427	} else {428		gid, e = lookupUserPrimaryGroup(username, domain)429		if e != nil {430			return nil, e431		}432	}433	// If this user has logged in at least once their home path should be stored434	// in the registry under the specified SID. References:435	// https://social.technet.microsoft.com/wiki/contents/articles/13895.how-to-remove-a-corrupted-user-profile-from-the-registry.aspx436	// https://support.asperasoft.com/hc/en-us/articles/216127438-How-to-delete-Windows-user-profiles437	//438	// The registry is the most reliable way to find the home path as the user439	// might have decided to move it outside of the default location,440	// (e.g. C:\users). Reference:441	// https://answers.microsoft.com/en-us/windows/forum/windows_7-security/how-do-i-set-a-home-directory-outside-cusers-for-a/aed68262-1bf4-4a4d-93dc-7495193a440f442	dir, e := findHomeDirInRegistry(uid)443	if e != nil {444		// If the home path does not exist in the registry, the user might445		// have not logged in yet; fall back to using getProfilesDirectory().446		// Find the username based on a SID and append that to the result of447		// getProfilesDirectory(). The domain is not relevant here.448		dir, e = getProfilesDirectory()449		if e != nil {450			return nil, e451		}452		dir += `\` + username453	}454	return newUser(uid, gid, dir, username, domain)455}456457func lookupUser(username string) (*User, error) {458	sid, _, t, e := syscall.LookupSID("", username)459	if e != nil {460		if errors.Is(e, windows.ERROR_NONE_MAPPED) {461			return nil, fmt.Errorf("%w: %w", UnknownUserError(username), e)462		}463		return nil, e464	}465	if !isValidUserAccountType(sid, t) {466		return nil, fmt.Errorf("user: should be user account type, not %d", t)467	}468	return newUserFromSid(sid)469}470471func lookupUserId(uid string) (*User, error) {472	sid, e := syscall.StringToSid(uid)473	if e != nil {474		return nil, e475	}476	return newUserFromSid(sid)477}478479func lookupGroup(groupname string) (*Group, error) {480	sid, err := lookupGroupName(groupname)481	if err != nil {482		return nil, err483	}484	return &Group{Name: groupname, Gid: sid}, nil485}486487func lookupGroupId(gid string) (*Group, error) {488	sid, err := syscall.StringToSid(gid)489	if err != nil {490		return nil, err491	}492	groupname, _, t, err := sid.LookupAccount("")493	if err != nil {494		return nil, err495	}496	if !isValidGroupAccountType(t) {497		return nil, fmt.Errorf("lookupGroupId: should be group account type, not %d", t)498	}499	return &Group{Name: groupname, Gid: gid}, nil500}501502func listGroups(user *User) ([]string, error) {503	var sids []string504	if u, err := Current(); err == nil && u.Uid == user.Uid {505		// It is faster and more reliable to get the groups506		// of the current user from the current process token.507		err := runAsProcessOwner(func() error {508			t, err := syscall.OpenCurrentProcessToken()509			if err != nil {510				return err511			}512			defer t.Close()513			groups, err := windows.GetTokenGroups(t)514			if err != nil {515				return err516			}517			for _, g := range groups.AllGroups() {518				sid, err := g.Sid.String()519				if err != nil {520					return err521				}522				sids = append(sids, sid)523			}524			return nil525		})526		if err != nil {527			return nil, err528		}529	} else {530		sid, err := syscall.StringToSid(user.Uid)531		if err != nil {532			return nil, err533		}534		username, domain, _, err := lookupUsernameAndDomain(sid)535		if err != nil {536			return nil, err537		}538		sids, err = listGroupsForUsernameAndDomain(username, domain)539		if err != nil {540			return nil, err541		}542	}543	// Add the primary group of the user to the list if it is not already there.544	// This is done only to comply with the POSIX concept of a primary group.545	for _, sid := range sids {546		if sid == user.Gid {547			return sids, nil548		}549	}550	return append(sids, user.Gid), nil551}

Code quality findings 9

Use of unsafe package detected; ensure it’s necessary, justified in comments, and bounds-checked to avoid memory corruption
warning safety unsafe-package
syscall.NetApiBufferFree((*byte)(unsafe.Pointer(domain)))
Use of unsafe package detected; ensure it’s necessary, justified in comments, and bounds-checked to avoid memory corruption
warning safety unsafe-package
i := (*syscall.UserInfo10)(unsafe.Pointer(p))
Defer inside loop; deferred calls accumulate until the function returns, not until the loop iteration ends. This can cause resource leaks
warning correctness defer-in-loop
defer syscall.NetApiBufferFree(p0)
Use of unsafe package detected; ensure it’s necessary, justified in comments, and bounds-checked to avoid memory corruption
warning safety unsafe-package
entries := (*[1024]windows.LocalGroupUserInfo0)(unsafe.Pointer(p0))[:entriesRead:entriesRead]
Use of unsafe package detected; ensure it’s necessary, justified in comments, and bounds-checked to avoid memory corruption
warning safety unsafe-package
i := (*windows.UserInfo4)(unsafe.Pointer(p))
Defer inside loop; deferred calls accumulate until the function returns, not until the loop iteration ends. This can cause resource leaks
warning correctness defer-in-loop
defer t.Close()
Multiple appends without pre-allocation; use make() with capacity when size is known
info performance append-without-prealloc
sids = append(sids, sid)
Multiple appends without pre-allocation; use make() with capacity when size is known
info performance append-without-prealloc
sids = append(sids, sid)
Multiple appends without pre-allocation; use make() with capacity when size is known
info performance append-without-prealloc
return append(sids, user.Gid), nil

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.