Linux: fix potential deadlock/segfault when calling into libnss for user mgmt (#295)

Export-Source-Commit: 12f28f2b1aa27c84df9f608557b8dc6f0de6cbb2
This commit is contained in:
Luke Okraszewski
2026-08-25 11:35:10 -05:00
committed by Gus Rivera
parent 6d906dc57b
commit 6dbccdd8ad
34 changed files with 325 additions and 150 deletions
+9
View File
@@ -393,6 +393,9 @@ linters:
- pattern: ^ssh\.(Dial|NewClient|NewClientConn)$
pkg: ^golang\.org/x/crypto/ssh$
msg: Use api/ssh package to construct SSH clients with proper defaults
- pkg: '^os/user$'
pattern: '^user\.(Lookup|LookupId|LookupGroup|LookupGroupId|Current|User\.GroupIds)$'
msg: 'os/user lookup APIs potentially unsafe; use the session/host/user wrapper instead'
- pattern: ^ssh\.(Dial|NewClientWithTimeout|NewClientConnWithTimeout)$
pkg: ^github\.com/gravitational/teleport/api/observability/tracing/ssh$
msg: Use api/ssh package to construct SSH clients with proper defaults
@@ -567,6 +570,12 @@ linters:
path: ^api/ssh/
text: tracessh.NewClientConnWithTimeout
# Allow session/host/user and tests to use os/user functions directly.
- path: '(_test\.go$|^api/|^integration/helpers/|^lib/utils/testutils|^session/host/user/)'
text: "session/host/user"
linters:
- forbidigo
# TODO: Fix forced type assertions in the rest of the codebase and remove this exclusion so that the linter can
# enforce this rule everywhere.
- linters:
+2 -2
View File
@@ -32,7 +32,6 @@ import (
"net"
"net/url"
"os"
"os/user"
"path/filepath"
"runtime"
"slices"
@@ -103,6 +102,7 @@ import (
"github.com/gravitational/teleport/lib/utils"
logutils "github.com/gravitational/teleport/lib/utils/log"
"github.com/gravitational/teleport/lib/utils/set"
hostuser "github.com/gravitational/teleport/session/host/user"
)
const (
@@ -5012,7 +5012,7 @@ func loopbackPool(proxyAddr string) *x509.CertPool {
// Username returns the current user's username
func Username() (string, error) {
u, err := user.Current()
u, err := hostuser.Current()
if err != nil {
return "", trace.Wrap(err)
}
+2 -2
View File
@@ -20,7 +20,6 @@ package mysql
import (
"os"
"os/user"
"path/filepath"
"strconv"
"strings"
@@ -29,6 +28,7 @@ import (
"gopkg.in/ini.v1"
"github.com/gravitational/teleport/lib/client/db/profile"
hostuser "github.com/gravitational/teleport/session/host/user"
)
func init() {
@@ -51,7 +51,7 @@ type OptionFile struct {
func DefaultConfigPath() (string, error) {
home, err := os.UserHomeDir()
if err != nil || home == "" {
usr, err := user.Current()
usr, err := hostuser.Current()
if err != nil {
return "", trace.ConvertSystemError(err)
}
+2 -2
View File
@@ -20,7 +20,6 @@ package postgres
import (
"os"
"os/user"
"path/filepath"
"strconv"
"strings"
@@ -29,6 +28,7 @@ import (
"gopkg.in/ini.v1"
"github.com/gravitational/teleport/lib/client/db/profile"
hostuser "github.com/gravitational/teleport/session/host/user"
)
func init() {
@@ -52,7 +52,7 @@ func defaultConfigPath() (string, error) {
// TODO(r0mant): Check PGSERVICEFILE and PGSYSCONFDIR env vars as well.
home, err := os.UserHomeDir()
if err != nil || home == "" {
usr, err := user.Current()
usr, err := hostuser.Current()
if err != nil {
return "", trace.ConvertSystemError(err)
}
+2 -2
View File
@@ -35,7 +35,6 @@ import (
"io/fs"
"log/slog"
"os/exec"
"os/user"
"strings"
"sync"
"unsafe"
@@ -46,6 +45,7 @@ import (
devicepb "github.com/gravitational/teleport/api/gen/proto/go/teleport/devicetrust/v1"
"github.com/gravitational/teleport/lib/darwin"
hostuser "github.com/gravitational/teleport/session/host/user"
)
func enrollDeviceInit() (*devicepb.EnrollDeviceInit, error) {
@@ -115,7 +115,7 @@ func collectDeviceData(_ CollectDataMode) (*devicepb.DeviceCollectedData, error)
return nil, trace.Wrap(statusErrorFromC(res))
}
osUser, err := user.Current()
osUser, err := hostuser.Current()
if err != nil {
return nil, trace.Wrap(err, "reading current user")
}
+2 -2
View File
@@ -29,7 +29,6 @@ import (
"log/slog"
"os"
"os/exec"
"os/user"
"time"
"github.com/google/go-attestation/attest"
@@ -39,6 +38,7 @@ import (
"github.com/gravitational/teleport"
devicepb "github.com/gravitational/teleport/api/gen/proto/go/teleport/devicetrust/v1"
"github.com/gravitational/teleport/lib/linux"
hostuser "github.com/gravitational/teleport/session/host/user"
)
// deviceStateFolderName starts without a "." on Linux systems.
@@ -167,7 +167,7 @@ func collectDeviceData(mode CollectDataMode) (*devicepb.DeviceCollectedData, err
baseBoardSerialNumber = dmiInfo.BoardSerial
}
u, err := user.Current()
u, err := hostuser.Current()
if err != nil {
return nil, trace.Wrap(err)
}
+2 -2
View File
@@ -25,7 +25,6 @@ import (
"net/http"
"net/url"
"os"
"os/user"
"path"
"path/filepath"
"strings"
@@ -34,6 +33,7 @@ import (
"github.com/gravitational/trace"
"github.com/gravitational/teleport/lib/defaults"
hostuser "github.com/gravitational/teleport/session/host/user"
)
type msalTokenCache struct {
@@ -51,7 +51,7 @@ type exchangeResponse struct {
// getRefreshTokens returns all current refresh tokens from the Azure CLI token cache.
func getRefreshTokens() ([]msalToken, error) {
usr, err := user.Current()
usr, err := hostuser.Current()
if err != nil {
return nil, trace.Wrap(err)
}
+2 -2
View File
@@ -23,13 +23,13 @@ import (
"fmt"
"os"
"os/exec"
"os/user"
"path/filepath"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/gravitational/trace"
"github.com/gravitational/teleport/lib/msgraph"
hostuser "github.com/gravitational/teleport/session/host/user"
)
// createGraphClient creates a new graph client from ambient credentials (Azure CLI credentials cache).
@@ -60,7 +60,7 @@ func EnsureAZLogin(ctx context.Context) error {
}
func getAzureDir() (string, error) {
usr, err := user.Current()
usr, err := hostuser.Current()
if err != nil {
return "", trace.Wrap(err)
}
@@ -43,11 +43,7 @@ import (
clusterconfigpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/clusterconfig/v1"
"github.com/gravitational/teleport/api/types/accessgraph"
"github.com/gravitational/teleport/api/utils/retryutils"
)
var (
// ErrUnsupportedPlatform is returned when the operating system is not supported.
ErrUnsupportedPlatform = errors.New("unsupported platform")
hostuser "github.com/gravitational/teleport/session/host/user"
)
// Watcher watches for changes to authorized_keys files
@@ -96,7 +92,7 @@ func NewWatcher(ctx context.Context, config WatcherConfig) (*Watcher, error) {
switch platform := getOS(config); platform {
case constants.LinuxOS, constants.DarwinOS:
default:
return nil, trace.Wrap(ErrUnsupportedPlatform)
return nil, trace.Wrap(hostuser.ErrUnsupportedPlatform)
}
if config.HostID == "" {
@@ -112,7 +108,7 @@ func NewWatcher(ctx context.Context, config WatcherConfig) (*Watcher, error) {
config.Clock = clockwork.NewRealClock()
}
if config.getHostUsers == nil {
config.getHostUsers = getHostUsers
config.getHostUsers = hostuser.GetHostUsers
}
w := &Watcher{
@@ -1,74 +0,0 @@
/*
* Teleport
* Copyright (C) 2024 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package authorizedkeys
/*
#define _XOPEN_SOURCE 500
#include <pwd.h>
*/
import "C"
import (
"os/user"
"strconv"
"sync"
"github.com/gravitational/trace"
)
// pwentLock should be acquired when using MT-Unsafe race:pwent functions (i.e.
// setpwent/getpwent/endpwent).
var pwentLock sync.Mutex
// getHostUsers returns the list of all users on the host from the user
// directory (depending on system configuration this can be /etc/passwd,
// LDAP...).
func getHostUsers() ([]user.User, error) {
pwentLock.Lock()
defer pwentLock.Unlock()
C.setpwent()
defer C.endpwent()
var results []user.User
for {
result, err := C.getpwent()
// cgo error convention, check the return value before errno
if result != nil {
results = append(results, passwdC2Go(result))
continue
}
if err != nil {
return nil, trace.Wrap(err)
}
return results, nil
}
}
// passwdC2Go converts `passwd` struct from C to golang native struct
func passwdC2Go(passwdC *C.struct_passwd) user.User {
name := C.GoString(passwdC.pw_name)
return user.User{
Name: name,
Username: name,
Uid: strconv.FormatUint(uint64(passwdC.pw_uid), 10),
Gid: strconv.FormatUint(uint64(passwdC.pw_gid), 10),
HomeDir: C.GoString(passwdC.pw_dir),
}
}
+3 -3
View File
@@ -26,7 +26,6 @@ import (
"maps"
"net"
"os"
"os/user"
"regexp"
"slices"
"strconv"
@@ -65,6 +64,7 @@ import (
"github.com/gravitational/teleport/lib/tlsca"
"github.com/gravitational/teleport/lib/utils"
logutils "github.com/gravitational/teleport/lib/utils/log"
hostuser "github.com/gravitational/teleport/session/host/user"
"github.com/gravitational/teleport/session/reexec"
)
@@ -749,11 +749,11 @@ func (sess *linuxSession) handleClientHello(m *tdpb.ClientHello) error {
}
func (sess *linuxSession) changeAuthorityFileOwnership(m *tdpb.ClientHello) error {
currentUser, err := user.Current()
currentUser, err := hostuser.Current()
if err != nil {
return trace.Wrap(err, "failed to get current user")
}
targetUser, err := user.Lookup(m.Username)
targetUser, err := hostuser.Lookup(m.Username)
if err != nil {
return trace.Wrap(err, "failed to lookup user %s", m.Username)
}
+2 -2
View File
@@ -23,7 +23,6 @@ import (
"log/slog"
"os"
"os/exec"
"os/user"
"path/filepath"
"regexp"
"strings"
@@ -36,6 +35,7 @@ import (
reexecutils "github.com/gravitational/teleport/lib/sshutils/reexec"
logutils "github.com/gravitational/teleport/lib/utils/log"
"github.com/gravitational/teleport/session/envutils"
hostuser "github.com/gravitational/teleport/session/host/user"
"github.com/gravitational/teleport/session/reexec"
"github.com/gravitational/teleport/session/reexec/reexecconstants"
)
@@ -126,7 +126,7 @@ func StartTeleportExecXSession(ctx context.Context, cfg *XSessionConfig) (*reexe
return nil, trace.BadParameter("missing parameter ChildLogConfig")
}
if _, err := user.Lookup(cfg.Login); err != nil {
if _, err := hostuser.Lookup(cfg.Login); err != nil {
return nil, trace.Wrap(err)
}
+2 -2
View File
@@ -26,7 +26,6 @@ import (
"net"
"os"
"os/exec"
"os/user"
"sync"
"syscall"
"time"
@@ -36,6 +35,7 @@ import (
"github.com/gravitational/teleport/lib/utils/mcputils"
hostutils "github.com/gravitational/teleport/session/host"
hostuser "github.com/gravitational/teleport/session/host/user"
)
// handleAuthErrStdio starts a stdio message reader and replies with the auth
@@ -238,7 +238,7 @@ func makeExecServerRunner(ctx context.Context, session *sessionHandler) (stdioSe
}
// Set host user.
hostUser, err := user.Lookup(mcpSpec.RunAsHostUser)
hostUser, err := hostuser.Lookup(mcpSpec.RunAsHostUser)
if err != nil {
return nil, trace.Wrap(err)
}
+2 -1
View File
@@ -75,6 +75,7 @@ import (
"github.com/gravitational/teleport/lib/sshutils"
reexecutils "github.com/gravitational/teleport/lib/sshutils/reexec"
"github.com/gravitational/teleport/lib/utils"
hostuser "github.com/gravitational/teleport/session/host/user"
"github.com/gravitational/teleport/session/networking"
"github.com/gravitational/teleport/session/networking/x11"
"github.com/gravitational/teleport/session/pam/pamcfg"
@@ -1103,7 +1104,7 @@ func (s *Server) startAuthorizedKeysManager(ctx context.Context, auth authclient
Clock: s.clock,
},
)
if errors.Is(err, authorizedkeysreporter.ErrUnsupportedPlatform) {
if errors.Is(err, hostuser.ErrUnsupportedPlatform) {
return nil
} else if err != nil {
return trace.Wrap(err)
+2 -1
View File
@@ -41,6 +41,7 @@ import (
tracessh "github.com/gravitational/teleport/api/observability/tracing/ssh"
rsession "github.com/gravitational/teleport/lib/session"
reexecutils "github.com/gravitational/teleport/lib/sshutils/reexec"
hostuser "github.com/gravitational/teleport/session/host/user"
"github.com/gravitational/teleport/session/reexec"
"github.com/gravitational/teleport/session/reexec/reexecconstants"
)
@@ -535,7 +536,7 @@ func getOwner(login string, lookupUser LookupUser, lookupGroup LookupGroup) (int
// setOwner changes the owner and mode of the TTY.
func (t *terminal) setOwner() error {
uid, gid, mode, err := getOwner(t.serverContext.Identity.Login, user.Lookup, user.LookupGroup)
uid, gid, mode, err := getOwner(t.serverContext.Identity.Login, hostuser.Lookup, hostuser.LookupGroup)
if err != nil {
return trace.Wrap(err)
}
+5 -4
View File
@@ -35,6 +35,7 @@ import (
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/teleport/session/host"
hostuser "github.com/gravitational/teleport/session/host/user"
)
// HostUsersProvisioningBackend is used to implement HostUsersBackend
@@ -74,7 +75,7 @@ func newHostSudoersBackend(uuid string) (HostSudoersBackend, error) {
// Lookup implements host user information lookup
func (*HostUsersProvisioningBackend) Lookup(username string) (*user.User, error) {
usr, err := user.Lookup(username)
usr, err := hostuser.Lookup(username)
if err != nil {
if !errors.Is(err, user.UnknownUserError(username)) && strings.Contains(err.Error(), "no such file or directory") {
return nil, trace.Wrap(err, "looking up user %q, sources configured for passwd in host's /etc/nsswitch.conf may be misconfigured", username)
@@ -88,17 +89,17 @@ func (*HostUsersProvisioningBackend) Lookup(username string) (*user.User, error)
// UserGIDs returns the list of group IDs for a user
func (*HostUsersProvisioningBackend) UserGIDs(u *user.User) ([]string, error) {
return u.GroupIds()
return hostuser.GroupIds(u)
}
// LookupGroup host group information lookup
func (*HostUsersProvisioningBackend) LookupGroup(name string) (*user.Group, error) {
return user.LookupGroup(name)
return hostuser.LookupGroup(name)
}
// LookupGroup host group information lookup by GID
func (*HostUsersProvisioningBackend) LookupGroupByID(gid string) (*user.Group, error) {
return user.LookupGroupId(gid)
return hostuser.LookupGroupId(gid)
}
// UpdateUser sets a user's groups and default shell, replacing their existing groups.
+2 -1
View File
@@ -29,6 +29,7 @@ import (
"github.com/gravitational/trace"
decisionpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/decision/v1alpha1"
hostuser "github.com/gravitational/teleport/session/host/user"
)
// ErrorContext contains context used to enrich child process launch errors.
@@ -65,7 +66,7 @@ func ChildErrorWithContext(errMsg string, context *ErrorContext) string {
unknownUserError := user.UnknownUserError(context.Login)
switch {
case strings.Contains(errMsg, "failed to open PAM context"): // PAM errors are often cause by an unknown user.
if _, err := user.Lookup(context.Login); errors.Is(err, unknownUserError) {
if _, err := hostuser.Lookup(context.Login); errors.Is(err, unknownUserError) {
if ambiguousHostUserDenial {
return ambiguousHostUserError()
}
+2 -2
View File
@@ -24,7 +24,6 @@ import (
"fmt"
"io/fs"
"os"
"os/user"
"path"
"path/filepath"
"strings"
@@ -37,6 +36,7 @@ import (
"github.com/gravitational/teleport/lib/tbot/botfs"
"github.com/gravitational/teleport/lib/tbot/internal/encoding"
"github.com/gravitational/teleport/lib/utils"
hostuser "github.com/gravitational/teleport/session/host/user"
)
const DirectoryType = "directory"
@@ -233,7 +233,7 @@ func (dd *Directory) ACLsEnabled() bool {
// This will not attempt to correct any issues, but will cause a hard failure if
// `acls: required` is configured and issues are detected.
func (dd *Directory) verifyLegacyACLs(keys []string) error {
currentUser, err := user.Current()
currentUser, err := hostuser.Current()
if err != nil {
// user.Current will fail if the user id does not exist in /etc/passwd
// as is the case with some containerized environments.
+4 -3
View File
@@ -39,6 +39,7 @@ import (
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/teleport/lib/utils/set"
hostuser "github.com/gravitational/teleport/session/host/user"
)
// mostACLRead is a permission mode granting readonly access to a file.
@@ -485,7 +486,7 @@ func resolveACLReaderSelector(s *ACLSelector, dir bool) (acl.Entry, error) {
}, nil
}
user, err := user.Lookup(s.User)
user, err := hostuser.Lookup(s.User)
if err != nil {
return acl.Entry{}, trace.Wrap(err)
}
@@ -506,7 +507,7 @@ func resolveACLReaderSelector(s *ACLSelector, dir bool) (acl.Entry, error) {
}, nil
}
group, err := user.LookupGroup(s.Group)
group, err := hostuser.LookupGroup(s.Group)
if err != nil {
return acl.Entry{}, trace.Wrap(err)
}
@@ -711,7 +712,7 @@ func GetOwner(fileInfo fs.FileInfo) (*user.User, error) {
return nil, trace.NotImplemented("Cannot verify file ownership on this platform.")
}
user, err := user.LookupId(strconv.Itoa(int(info.Uid)))
user, err := hostuser.LookupId(strconv.Itoa(int(info.Uid)))
if err != nil {
return nil, trace.Wrap(err)
}
@@ -23,7 +23,6 @@ import (
"fmt"
"log/slog"
"os"
"os/user"
"path/filepath"
"slices"
"strings"
@@ -42,6 +41,7 @@ import (
"github.com/gravitational/teleport/lib/teleterm/clusters"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/teleport/lib/utils/hostid"
hostuser "github.com/gravitational/teleport/session/host/user"
)
type RoleSetup struct {
@@ -98,7 +98,7 @@ func (s *RoleSetup) Run(ctx context.Context, accessAndIdentity AccessAndIdentity
}
}
systemUser, err := user.Current()
systemUser, err := hostuser.Current()
if err != nil {
return noCertsReloaded, trace.Wrap(err)
}
+5 -3
View File
@@ -30,6 +30,8 @@ import (
"syscall"
"github.com/gravitational/trace"
hostuser "github.com/gravitational/teleport/session/host/user"
)
// PercentUsed returns percentage of disk space used. The percentage of disk
@@ -93,12 +95,12 @@ func CanUserWriteTo(path string) (bool, error) {
var usr *user.User
if ogUser := os.Getenv("SUDO_USER"); ogUser != "" {
usr, err = user.Lookup(ogUser)
usr, err = hostuser.Lookup(ogUser)
if err != nil {
return false, trace.NotFound("could not determine original user: %+v", err)
}
} else {
usr, err = user.Current()
usr, err = hostuser.Current()
if err != nil {
return false, trace.NotFound("could not determine current user: %+v", err)
}
@@ -116,7 +118,7 @@ func CanUserWriteTo(path string) (bool, error) {
}
// file and user have a group in common
groupIDs, err := usr.GroupIds()
groupIDs, err := hostuser.GroupIds(usr)
if err != nil {
return false, trace.NotFound("could not determine current user group ids: %+v", err)
}
+4 -2
View File
@@ -34,6 +34,8 @@ import (
"syscall"
"github.com/gravitational/trace"
hostuser "github.com/gravitational/teleport/session/host/user"
)
// man GROUPADD(8), exit codes section
@@ -181,7 +183,7 @@ func UserDel(username string) (exitCode int, err error) {
if err != nil {
return -1, trace.NotFound("cant find userdel binary: %s", err)
}
u, err := user.Lookup(username)
u, err := hostuser.Lookup(username)
if err != nil {
return -1, trace.Wrap(err)
}
@@ -390,7 +392,7 @@ func GetHostUserCredential(localUser *user.User) (*syscall.Credential, error) {
}
// Lookup supplementary groups for the user.
userGroups, err := localUser.GroupIds()
userGroups, err := hostuser.GroupIds(localUser)
if err != nil {
return nil, trace.Wrap(err)
}
+22
View File
@@ -0,0 +1,22 @@
// Teleport
// Copyright (C) 2026 Gravitational, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package user
import "errors"
// ErrUnsupportedPlatform is returned when the operating system is not supported.
var ErrUnsupportedPlatform = errors.New("unsupported platform")
+42
View File
@@ -0,0 +1,42 @@
// Teleport
// Copyright (C) 2026 Gravitational, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// Package user provides a thin concurrency safe wrapper around the
// `os/user` APIs for Linux when CGO is enabled.
//
// On Linux, the `os/user` package may call into C-based NSS backends via cgo.
// Some NSS implementations (or configurations) are not safe for concurrent
// use and can deadlock or crash when lookups are performed concurrently.
//
// To avoid such failures, this package serializes calls to a small set of `os/user`
// APIs on linux+cgo builds.
// Non-Linux or non-cgo builds forward calls to the stdlib without locking.
//
// References:
//
// - Debian bug report: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=831390
// - Teleport issue: https://github.com/gravitational/teleport/issues/69662
//
// Note that this package is placed in `session/host` specifically because the `session` tree
// is effectively a submodule within Teleport which guards against any imports from Teleport itself.
// This was done to optimize the session helper load times and avoid bleeding runtime dependencies into the
// session helper which would incease the load time and degrade latency. Since Teleport still calls
// into the session helper code in the same process for which a mutex must be shared, the user package
// is placed here despite less than ideal code organization.
package user
// TODO(okraport): revisit this module placement and investigate moving it to lib/utils/user if possible.
// TODO(okraport): revisit this module to consider replacement with getent process shell out to avoid the mutex.
@@ -16,7 +16,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package authorizedkeys
package user
/*
#include <pwd.h>
@@ -31,10 +31,10 @@ import (
"github.com/gravitational/trace"
)
// getHostUsers returns the list of all users on the host from the user
// GetHostUsers returns the list of all users on the host from the user
// directory (depending on system configuration this can be /etc/passwd,
// LDAP...).
func getHostUsers() ([]user.User, error) {
func GetHostUsers() ([]user.User, error) {
// on darwin the setpwent/getpwent/endpwent functions use thread-local
// storage so there's no need for a global lock but we must call the whole
// sequence from the same thread
+48
View File
@@ -0,0 +1,48 @@
//go:build !linux || !cgo
// Teleport
// Copyright (C) 2026 Gravitational, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package user
import (
osu "os/user"
"github.com/gravitational/trace"
)
// Lookup wraps [os/user.Lookup].
func Lookup(username string) (*osu.User, error) { return osu.Lookup(username) }
// LookupId wraps [os/user.LookupId].
func LookupId(id string) (*osu.User, error) { return osu.LookupId(id) }
// LookupGroup wraps [os/user.LookupGroup].
func LookupGroup(name string) (*osu.Group, error) { return osu.LookupGroup(name) }
// LookupGroupId wraps [os/user.LookupGroupId].
func LookupGroupId(id string) (*osu.Group, error) { return osu.LookupGroupId(id) }
// Current wraps [os/user.Current].
func Current() (*osu.User, error) { return osu.Current() }
// GroupIds wraps [os/user.User.GroupIds].
func GroupIds(u *osu.User) ([]string, error) {
if u == nil {
return nil, trace.BadParameter("user cannot be nil")
}
return u.GroupIds()
}
+120
View File
@@ -0,0 +1,120 @@
//go:build linux && cgo
// Teleport
// Copyright (C) 2026 Gravitational, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package user
/*
#define _XOPEN_SOURCE 500
#include <pwd.h>
*/
import "C"
import (
osuser "os/user"
"strconv"
"sync"
"github.com/gravitational/trace"
)
var packageMU sync.Mutex
// Lookup wraps [os/user.Lookup] with process-wide serialization.
func Lookup(username string) (*osuser.User, error) {
packageMU.Lock()
defer packageMU.Unlock()
return osuser.Lookup(username)
}
// LookupId wraps [os/user.LookupId] with process-wide serialization.
func LookupId(id string) (*osuser.User, error) {
packageMU.Lock()
defer packageMU.Unlock()
return osuser.LookupId(id)
}
// LookupGroup wraps [os/user.LookupGroup] with process-wide serialization.
func LookupGroup(name string) (*osuser.Group, error) {
packageMU.Lock()
defer packageMU.Unlock()
return osuser.LookupGroup(name)
}
// LookupGroupId wraps [os/user.LookupGroupId] with process-wide serialization.
func LookupGroupId(id string) (*osuser.Group, error) {
packageMU.Lock()
defer packageMU.Unlock()
return osuser.LookupGroupId(id)
}
// Current wraps [os/user.Current] with process-wide serialization.
func Current() (*osuser.User, error) {
packageMU.Lock()
defer packageMU.Unlock()
return osuser.Current()
}
// LookGroupIdsup wraps [os/user.User.GroupIds] with process-wide serialization.
func GroupIds(u *osuser.User) ([]string, error) {
if u == nil {
return nil, trace.BadParameter("user cannot be nil")
}
packageMU.Lock()
defer packageMU.Unlock()
return u.GroupIds()
}
// GetHostUsers returns the list of all users on the host from the user
// directory (depending on system configuration this can be /etc/passwd,
// LDAP...).
func GetHostUsers() ([]osuser.User, error) {
// A lock should be acquired when using MT-Unsafe race:pwent functions (i.e.
// setpwent/getpwent/endpwent). Since `getpwent` can follow the same path
// into libnss we take the package lock.
packageMU.Lock()
defer packageMU.Unlock()
C.setpwent()
defer C.endpwent()
var results []osuser.User
for {
result, err := C.getpwent()
// cgo error convention, check the return value before errno
if result != nil {
results = append(results, passwdC2Go(result))
continue
}
if err != nil {
return nil, trace.Wrap(err)
}
return results, nil
}
}
// passwdC2Go converts `passwd` struct from C to golang native struct
func passwdC2Go(passwdC *C.struct_passwd) osuser.User {
name := C.GoString(passwdC.pw_name)
return osuser.User{
Name: name,
Username: name,
Uid: strconv.FormatUint(uint64(passwdC.pw_uid), 10),
Gid: strconv.FormatUint(uint64(passwdC.pw_gid), 10),
HomeDir: C.GoString(passwdC.pw_dir),
}
}
@@ -16,7 +16,7 @@
//go:build !darwin && !linux
package authorizedkeys
package user
import (
"os/user"
@@ -29,7 +29,7 @@ var alwaysFalse bool
// getHostUsers returns ErrUnsupportedPlatform because this platform is not
// supported. On supported platforms, it returns the list of all users on the
// host from the user directory.
func getHostUsers() ([]user.User, error) {
func GetHostUsers() ([]user.User, error) {
if alwaysFalse {
// thwart the well-meaning intentions of staticcheck
return nil, nil
+9 -8
View File
@@ -48,6 +48,7 @@ import (
"github.com/gravitational/teleport/session/auditd"
"github.com/gravitational/teleport/session/envutils"
"github.com/gravitational/teleport/session/host"
hostuser "github.com/gravitational/teleport/session/host/user"
"github.com/gravitational/teleport/session/logconstants"
"github.com/gravitational/teleport/session/loginuid"
"github.com/gravitational/teleport/session/networking"
@@ -468,7 +469,7 @@ func RunCommand() (exitErr error, err error) {
WtmpdbFile: c.UaccMetadata.WtmpdbPath,
})
localUser, err := user.Lookup(c.Login)
localUser, err := hostuser.Lookup(c.Login)
if err != nil {
if uaccErr := uaccHandler.FailedLogin(c.Login, &c.UaccMetadata.RemoteAddr); uaccErr != nil {
slog.DebugContext(ctx, "unable to write failed login attempt to uacc", "error", uaccErr)
@@ -671,8 +672,8 @@ type osWrapper struct {
func newOsWrapper() *osWrapper {
return &osWrapper{
LookupGroup: user.LookupGroup,
LookupUser: user.Lookup,
LookupGroup: hostuser.LookupGroup,
LookupUser: hostuser.Lookup,
CommandContext: exec.CommandContext,
}
}
@@ -698,7 +699,7 @@ func (s *systemUser) UID() string {
}
func (s *systemUser) GroupIds() ([]string, error) {
return s.u.GroupIds()
return hostuser.GroupIds(s.u)
}
// startNewParker starts a new parker process only if the requested user has been created
@@ -810,7 +811,7 @@ func RunNetworking() (code int, err error) {
// Once the PAM stack is called with parent process permissions, set the process uid
// and gid to the requested user. This way, the user's networking requests will be
// done with the user's permissions.
localUser, err := user.Lookup(c.Login)
localUser, err := hostuser.Lookup(c.Login)
if err != nil {
return reexecconstants.RemoteCommandFailure, trace.NotFound("%s", err)
}
@@ -1231,7 +1232,7 @@ func openFileAsUser(localUser *user.User, path string) (file *os.File, err error
return nil, trace.Wrap(err)
}
strIDs, err := localUser.GroupIds()
strIDs, err := hostuser.GroupIds(localUser)
if err != nil {
return nil, trace.Wrap(err)
}
@@ -1480,7 +1481,7 @@ var accessibleHomeDirMu sync.Mutex
// hasAccessibleHomeDir checks if the current user has access to an existing home directory.
func hasAccessibleHomeDir() error {
// this should usually be fetching a cached value
currentUser, err := user.Current()
currentUser, err := hostuser.Current()
if err != nil {
return trace.Wrap(err)
}
@@ -1522,7 +1523,7 @@ func hasAccessibleHomeDir() error {
// errors will be returned, which means a missing, inaccessible, or otherwise invalid home directory will result
// in a return of (false, nil)
func checkHomeDir(localUser *user.User) (bool, error) {
currentUser, err := user.Current()
currentUser, err := hostuser.Current()
if err != nil {
return false, trace.Wrap(err)
}
+2 -2
View File
@@ -26,7 +26,6 @@ import (
"io"
"log/slog"
"os"
"os/user"
"path"
"path/filepath"
"strings"
@@ -38,6 +37,7 @@ import (
"github.com/pkg/sftp"
"golang.org/x/sys/unix"
hostuser "github.com/gravitational/teleport/session/host/user"
"github.com/gravitational/teleport/session/reexec/safefile"
"github.com/gravitational/teleport/session/sftputils"
)
@@ -305,7 +305,7 @@ func RunSFTP(logger *slog.Logger) error {
}
defer auditFile.Close()
currentUser, err := user.Current()
currentUser, err := hostuser.Current()
if err != nil {
return trace.Wrap(err)
}
+3 -2
View File
@@ -37,18 +37,19 @@ import "C"
import (
"context"
"log/slog"
"os/user"
"strings"
"syscall"
"unsafe"
"github.com/gravitational/trace"
hostuser "github.com/gravitational/teleport/session/host/user"
)
// getLoginShell determines the login shell for a given username
func getLoginShell(username string) (string, error) {
// See if the username is valid.
_, err := user.Lookup(username)
_, err := hostuser.Lookup(username)
if err != nil {
return "", trace.Wrap(err)
}
+9 -8
View File
@@ -37,6 +37,7 @@ import (
"github.com/gravitational/teleport/lib/tbot/cli"
"github.com/gravitational/teleport/lib/tbot/config"
"github.com/gravitational/teleport/lib/tbot/identity"
hostuser "github.com/gravitational/teleport/session/host/user"
)
// RootUID is the UID of the root user
@@ -207,7 +208,7 @@ func ensurePermissions(
return trace.BadParameter("File %s is expected to be a file but is a directory", path)
}
currentUser, err := user.Current()
currentUser, err := hostuser.Current()
if err != nil {
return trace.Wrap(err)
}
@@ -319,12 +320,12 @@ func parseOwnerString(owner string) (*user.User, *user.Group, error) {
return nil, nil, trace.BadParameter("invalid owner string: %q", owner)
}
ownerUser, err := user.Lookup(ownerParts[0])
ownerUser, err := hostuser.Lookup(ownerParts[0])
if err != nil {
return nil, nil, trace.Wrap(err)
}
ownerGroup, err := user.LookupGroup(ownerParts[1])
ownerGroup, err := hostuser.LookupGroup(ownerParts[1])
if err != nil {
return nil, nil, trace.Wrap(err)
}
@@ -358,12 +359,12 @@ func getOwner(cliOwner, defaultOwner string) (*user.User, *user.Group, error) {
log.DebugContext(context.TODO(), "Will use current user as owner")
// Otherwise, return the current user and group
currentUser, err := user.Current()
currentUser, err := hostuser.Current()
if err != nil {
return nil, nil, trace.Wrap(err)
}
currentGroup, err := user.LookupGroupId(currentUser.Gid)
currentGroup, err := hostuser.LookupGroupId(currentUser.Gid)
if err != nil {
return nil, nil, trace.Wrap(err)
}
@@ -383,17 +384,17 @@ func getAndTestACLOptions(initCmd *cli.InitCommand, destDir string) (*user.User,
return nil, nil, nil, trace.BadParameter("--reader-user must be set")
}
botUser, err := user.Lookup(initCmd.BotUser)
botUser, err := hostuser.Lookup(initCmd.BotUser)
if err != nil {
return nil, nil, nil, trace.Wrap(err)
}
botGroup, err := user.LookupGroupId(botUser.Gid)
botGroup, err := hostuser.LookupGroupId(botUser.Gid)
if err != nil {
return nil, nil, nil, trace.Wrap(err)
}
readerUser, err := user.Lookup(initCmd.ReaderUser)
readerUser, err := hostuser.Lookup(initCmd.ReaderUser)
if err != nil {
return nil, nil, nil, trace.Wrap(err)
}
+2 -2
View File
@@ -27,7 +27,6 @@ import (
"maps"
"net/url"
"os"
"os/user"
"path"
"path/filepath"
"runtime"
@@ -61,6 +60,7 @@ import (
"github.com/gravitational/teleport/lib/utils"
logutils "github.com/gravitational/teleport/lib/utils/log"
"github.com/gravitational/teleport/lib/versioncontrol"
hostuser "github.com/gravitational/teleport/session/host/user"
"github.com/gravitational/teleport/session/reexec"
"github.com/gravitational/teleport/session/reexec/reexecconstants"
"github.com/gravitational/teleport/session/selinux"
@@ -1230,7 +1230,7 @@ func onSCP(scpFlags *scp.Flags) error {
}
// get user's home dir (it serves as a default destination)
user, err := user.Current()
user, err := hostuser.Current()
if err != nil {
return trace.Wrap(err)
}
+2 -2
View File
@@ -33,7 +33,6 @@ import (
"os"
"os/exec"
"os/signal"
"os/user"
"path/filepath"
"regexp"
"runtime"
@@ -106,6 +105,7 @@ import (
"github.com/gravitational/teleport/lib/utils/mlock"
"github.com/gravitational/teleport/lib/utils/parse"
stacksignal "github.com/gravitational/teleport/lib/utils/signal"
hostuser "github.com/gravitational/teleport/session/host/user"
"github.com/gravitational/teleport/session/networking/x11"
"github.com/gravitational/teleport/session/shell"
"github.com/gravitational/teleport/tool/common"
@@ -4453,7 +4453,7 @@ func onSSHLatency(cf *CLIConf) error {
func runLocalCommand(hostLogin string, command []string) error {
if len(command) == 0 {
if hostLogin == "" {
user, err := user.Current()
user, err := hostuser.Current()
if err != nil {
return trace.Wrap(err)
}