Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add local user account creation for host process containers #1286

Merged
merged 3 commits into from
Feb 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions cmd/containerd-shim-runhcs-v1/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"github.com/Microsoft/hcsshim/internal/hcs"
"github.com/Microsoft/hcsshim/internal/oc"
"github.com/Microsoft/hcsshim/internal/winapi"
"github.com/containerd/containerd/runtime/v2/task"
"github.com/gogo/protobuf/proto"
"github.com/pkg/errors"
Expand Down Expand Up @@ -49,8 +50,8 @@ The delete command will be executed in the container's bundle as its cwd.
SkipArgReorder: true,
Action: func(context *cli.Context) (err error) {
// We cant write anything to stdout for this cmd other than the
// task.DeleteResponse by protcol. We can write to stderr which will be
// warning logged in containerd.
// task.DeleteResponse by protocol. We can write to stderr which will be
// logged as a warning in containerd.

ctx, span := trace.StartSpan(gcontext.Background(), "delete")
defer span.End()
Expand Down Expand Up @@ -99,6 +100,22 @@ The delete command will be executed in the container's bundle as its cwd.
}
}

// For Host Process containers if a group name is passed as the user for the container the shim will create a
// temporary user for the container to run as and add it to the specified group. On container exit the account will
// be deleted, but if the shim crashed unexpectedly (panic, terminated etc.) then the account may still be around.
// The username will be the container ID so try and delete it here. The username character limit is 20, so we need to
// slice down the container ID a bit.
username := idFlag[:winapi.UserNameCharLimit]

// Always try and delete the user, if it doesn't exist we'll get a specific error code that we can use to
// not log any warnings.
if err := winapi.NetUserDel(
"",
username,
); err != nil && err != winapi.NERR_UserNotFound {
fmt.Fprintf(os.Stderr, "failed to delete user %q: %v", username, err)
}

if data, err := proto.Marshal(&task.DeleteResponse{
ExitedAt: time.Now(),
ExitStatus: 255,
Expand Down
75 changes: 50 additions & 25 deletions internal/jobcontainers/jobcontainer.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,18 @@ type initProc struct {

// JobContainer represents a lightweight container composed from a job object.
type JobContainer struct {
id string
spec *specs.Spec // OCI spec used to create the container
job *jobobject.JobObject // Object representing the job object the container owns
sandboxMount string // Path to where the sandbox is mounted on the host
closedWaitOnce sync.Once
init initProc
startTimestamp time.Time
exited chan struct{}
waitBlock chan struct{}
waitError error
id string
spec *specs.Spec // OCI spec used to create the container
job *jobobject.JobObject // Object representing the job object the container owns
sandboxMount string // Path to where the sandbox is mounted on the host
closedWaitOnce sync.Once
init initProc
token windows.Token
localUserAccount string
startTimestamp time.Time
exited chan struct{}
waitBlock chan struct{}
waitError error
}

var _ cow.ProcessHost = &JobContainer{}
Expand Down Expand Up @@ -205,21 +207,23 @@ func (c *JobContainer) CreateProcess(ctx context.Context, config interface{}) (_
return nil, errors.Wrapf(err, "failed to get application name from commandline %q", conf.CommandLine)
}

var token windows.Token
if inheritUserTokenIsSet(c.spec.Annotations) {
token, err = openCurrentProcessToken()
if err != nil {
return nil, err
}
} else {
token, err = processToken(conf.User)
if err != nil {
return nil, errors.Wrap(err, "failed to create user process token")
// If we haven't grabbed a token yet this is the init process being launched. Skip grabbing another token afterwards if we've already
// done the work (c.token != 0), this would typically be for an exec being launched.
if c.token == 0 {
dcantah marked this conversation as resolved.
Show resolved Hide resolved
katiewasnothere marked this conversation as resolved.
Show resolved Hide resolved
if inheritUserTokenIsSet(c.spec.Annotations) {
c.token, err = openCurrentProcessToken()
if err != nil {
return nil, err
}
} else {
c.token, err = c.processToken(ctx, conf.User)
if err != nil {
return nil, fmt.Errorf("failed to create user process token: %w", err)
}
}
}
defer token.Close()

env, err := defaultEnvBlock(token)
env, err := defaultEnvBlock(c.token)
if err != nil {
return nil, errors.Wrap(err, "failed to get default environment block")
}
Expand Down Expand Up @@ -257,7 +261,7 @@ func (c *JobContainer) CreateProcess(ctx context.Context, config interface{}) (_
commandLine,
exec.WithDir(workDir),
exec.WithEnv(env),
exec.WithToken(token),
exec.WithToken(c.token),
exec.WithJobObject(c.job),
exec.WithConPty(cpty),
exec.WithProcessFlags(windows.CREATE_BREAKAWAY_FROM_JOB),
Expand Down Expand Up @@ -314,15 +318,36 @@ func (c *JobContainer) Start(ctx context.Context) error {
return nil
}

// Close closes any open handles.
// Close free's up any resources (handles, temporary accounts).
func (c *JobContainer) Close() error {
// Do not return the first error so we can finish cleaning up.

var closeErr bool
if err := c.job.Close(); err != nil {
return err
log.G(context.Background()).WithError(err).WithField("cid", c.id).Warning("failed to close job object")
closeErr = true
}

if err := c.token.Close(); err != nil {
log.G(context.Background()).WithError(err).WithField("cid", c.id).Warning("failed to close token")
closeErr = true
}

// Delete the containers local account if one was created
if c.localUserAccount != "" {
if err := winapi.NetUserDel("", c.localUserAccount); err != nil {
log.G(context.Background()).WithError(err).WithField("cid", c.id).Warning("failed to delete local account")
closeErr = true
}
}

c.closedWaitOnce.Do(func() {
c.waitError = hcs.ErrAlreadyClosed
close(c.waitBlock)
})
if closeErr {
return errors.New("failed to close one or more job container resources")
}
return nil
}

Expand Down
124 changes: 114 additions & 10 deletions internal/jobcontainers/logon.go
Original file line number Diff line number Diff line change
@@ -1,35 +1,139 @@
package jobcontainers

import (
"context"
"fmt"
"strings"
"unsafe"

"github.com/Microsoft/go-winio/pkg/guid"
"github.com/Microsoft/hcsshim/internal/log"
"github.com/Microsoft/hcsshim/internal/winapi"
"github.com/pkg/errors"
"golang.org/x/sys/windows"
)

// processToken returns a user token for the user specified by `user`. This should be in the form
// of either a DOMAIN\username or just username.
func processToken(user string) (windows.Token, error) {
func randomPswd() (*uint16, error) {
g, err := guid.NewV4()
if err != nil {
return nil, err
}
return windows.UTF16PtrFromString(g.String())
}

func groupExists(groupName string) bool {
var p *byte
if err := winapi.NetLocalGroupGetInfo(
"",
groupName,
1,
&p,
); err != nil {
return false
}
defer windows.NetApiBufferFree(p)
return true
}

// makeLocalAccount creates a local account with the passed in username and a randomly generated password.
// The user specified by `user`` will added to the `groupName`. This function does not check if groupName exists, that must be handled
// the caller.
func makeLocalAccount(ctx context.Context, user, groupName string) (_ *uint16, err error) {
// Create a local account with a random password
pswd, err := randomPswd()
if err != nil {
return nil, fmt.Errorf("failed to generate random password: %w", err)
}

userUTF16, err := windows.UTF16PtrFromString(user)
if err != nil {
return nil, fmt.Errorf("failed to encode username to UTF16: %w", err)
}

usr1 := &winapi.UserInfo1{
Name: userUTF16,
Password: pswd,
Priv: winapi.USER_PRIV_USER,
Flags: winapi.UF_NORMAL_ACCOUNT | winapi.UF_DONT_EXPIRE_PASSWD,
}
if err := winapi.NetUserAdd(
"",
1,
(*byte)(unsafe.Pointer(usr1)),
nil,
); err != nil {
return nil, fmt.Errorf("failed to create user %s: %w", user, err)
}
defer func() {
if err != nil {
_ = winapi.NetUserDel("", user)
}
}()

log.G(ctx).WithField("username", user).Debug("Created local user account for job container")
dcantah marked this conversation as resolved.
Show resolved Hide resolved

sid, _, _, err := windows.LookupSID("", user)
if err != nil {
return nil, fmt.Errorf("failed to lookup SID for user %q: %w", user, err)
}

sids := []winapi.LocalGroupMembersInfo0{{Sid: sid}}
if err := winapi.NetLocalGroupAddMembers(
dcantah marked this conversation as resolved.
Show resolved Hide resolved
"",
groupName,
0,
(*byte)(unsafe.Pointer(&sids[0])),
1,
); err != nil {
return nil, fmt.Errorf("failed to add user %q to the %q group: %w", user, groupName, err)
}

return pswd, nil
}

// processToken verifies first whether userOrGroup is a username or group name. If it's a valid group name,
// a temporary local user account will be created and added to the group and then the token for the user will
// be returned. If it is not a group name then the user will logged into and the token will be returned.
func (c *JobContainer) processToken(ctx context.Context, userOrGroup string) (windows.Token, error) {
var (
domain string
userName string
token windows.Token
)

split := strings.Split(user, "\\")
if userOrGroup == "" {
return 0, errors.New("empty username or group name passed")
}

if groupExists(userOrGroup) {
katiewasnothere marked this conversation as resolved.
Show resolved Hide resolved
userName = c.id[:winapi.UserNameCharLimit]
pswd, err := makeLocalAccount(ctx, userName, userOrGroup)
if err != nil {
return 0, fmt.Errorf("failed to create local account for container: %w", err)
}
if err := winapi.LogonUser(
windows.StringToUTF16Ptr(userName),
nil,
pswd,
winapi.LOGON32_LOGON_INTERACTIVE,
winapi.LOGON32_PROVIDER_DEFAULT,
&token,
); err != nil {
return 0, fmt.Errorf("failed to logon user: %w", err)
}
c.localUserAccount = userName
return token, nil
}

// Must be a user string, split it by domain and username
split := strings.Split(userOrGroup, "\\")
if len(split) == 2 {
domain = split[0]
userName = split[1]
} else if len(split) == 1 {
userName = split[0]
} else {
return 0, fmt.Errorf("invalid user string `%s`", user)
}

if user == "" {
return 0, errors.New("empty user string passed")
return 0, fmt.Errorf("invalid user string `%s`", userOrGroup)
}

logonType := winapi.LOGON32_LOGON_INTERACTIVE
Expand All @@ -46,7 +150,7 @@ func processToken(user string) (windows.Token, error) {
winapi.LOGON32_PROVIDER_DEFAULT,
&token,
); err != nil {
return 0, errors.Wrap(err, "failed to logon user")
return 0, fmt.Errorf("failed to logon user: %w", err)
}
return token, nil
}
Expand Down
3 changes: 3 additions & 0 deletions internal/winapi/jobobject.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ type JOBOBJECT_ASSOCIATE_COMPLETION_PORT struct {
// JOBOBJECT_IO_RATE_CONTROL_INFORMATION **InfoBlocks,
// ULONG *InfoBlockCount
// );
//
//sys QueryIoRateControlInformationJobObject(jobHandle windows.Handle, volumeName *uint16, ioRateControlInfo **JOBOBJECT_IO_RATE_CONTROL_INFORMATION, infoBlockCount *uint32) (ret uint32, err error) = kernel32.QueryIoRateControlInformationJobObject

// NTSTATUS
Expand All @@ -203,6 +204,7 @@ type JOBOBJECT_ASSOCIATE_COMPLETION_PORT struct {
// _In_ ACCESS_MASK DesiredAccess,
// _In_ POBJECT_ATTRIBUTES ObjectAttributes
// );
//
//sys NtOpenJobObject(jobHandle *windows.Handle, desiredAccess uint32, objAttributes *ObjectAttributes) (status uint32) = ntdll.NtOpenJobObject

// NTSTATUS
Expand All @@ -212,4 +214,5 @@ type JOBOBJECT_ASSOCIATE_COMPLETION_PORT struct {
// _In_ ACCESS_MASK DesiredAccess,
// _In_opt_ POBJECT_ATTRIBUTES ObjectAttributes
// );
//
//sys NtCreateJobObject(jobHandle *windows.Handle, desiredAccess uint32, objAttributes *ObjectAttributes) (status uint32) = ntdll.NtCreateJobObject
1 change: 1 addition & 0 deletions internal/winapi/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ package winapi
// LPWSTR lpBuffer,
// LPWSTR *lpFilePart
// );
//
//sys SearchPath(lpPath *uint16, lpFileName *uint16, lpExtension *uint16, nBufferLength uint32, lpBuffer *uint16, lpFilePath *uint16) (size uint32, err error) = kernel32.SearchPathW
1 change: 1 addition & 0 deletions internal/winapi/system.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const STATUS_INFO_LENGTH_MISMATCH = 0xC0000004
// ULONG SystemInformationLength,
// PULONG ReturnLength
// );
//
//sys NtQuerySystemInformation(systemInfoClass int, systemInformation uintptr, systemInfoLength uint32, returnLength *uint32) (status uint32) = ntdll.NtQuerySystemInformation

type SYSTEM_PROCESS_INFORMATION struct {
Expand Down
1 change: 1 addition & 0 deletions internal/winapi/thread.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ package winapi
// DWORD dwCreationFlags,
// LPDWORD lpThreadId
// );
//
//sys CreateRemoteThread(process windows.Handle, sa *windows.SecurityAttributes, stackSize uint32, startAddr uintptr, parameter uintptr, creationFlags uint32, threadID *uint32) (handle windows.Handle, err error) = kernel32.CreateRemoteThread
Loading