mirror of
https://github.com/kemko/nomad.git
synced 2026-01-01 16:05:42 +03:00
In #25496 we introduced the ability to have `task.user` set for on Windows, so long as the user ID fits a particular shape. But this uncovered a 7 year old bug in the `java` driver introduced in #5143, where we set the `task.user` to the non-existent Unix user `nobody`, even if we're running on Windows. Prior to the change in #25496 we always ignored the `task.user`, so this was not a problem. We don't set the `task.user` in the `raw_exec` driver, and the otherwise very similar `exec` driver is Linux-only, so we never see the problem there. Fix the bug in the `java` driver by gating the change to the `task.user` on not being Windows. Also add a check to the new code path that the user is non-empty before parsing it, so that any third party drivers that might be borrowing the executor code don't hit the same probem on Windows. Ref: https://github.com/hashicorp/nomad/pull/5143 Ref: https://github.com/hashicorp/nomad/pull/25496 Fixes: https://github.com/hashicorp/nomad/issues/25638
196 lines
5.5 KiB
Go
196 lines
5.5 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
//go:build windows
|
|
|
|
package executor
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"runtime"
|
|
"strings"
|
|
"syscall"
|
|
"unsafe"
|
|
|
|
hclog "github.com/hashicorp/go-hclog"
|
|
"github.com/hashicorp/go-set/v3"
|
|
"github.com/hashicorp/nomad/client/lib/cpustats"
|
|
"github.com/hashicorp/nomad/drivers/shared/executor/procstats"
|
|
"github.com/hashicorp/nomad/plugins/drivers"
|
|
"golang.org/x/sys/windows"
|
|
)
|
|
|
|
func NewExecutorWithIsolation(logger hclog.Logger, compute cpustats.Compute) Executor {
|
|
logger = logger.Named("executor")
|
|
logger.Error("isolation executor is not supported on this platform, using default")
|
|
return NewExecutor(logger, compute)
|
|
}
|
|
|
|
func (e *UniversalExecutor) configureResourceContainer(_ *ExecCommand, _ int) (func() error, func(), error) {
|
|
cleanup := func() {}
|
|
running := func() error { return nil }
|
|
return running, cleanup, nil
|
|
}
|
|
|
|
func (e *UniversalExecutor) start(command *ExecCommand) error {
|
|
return e.childCmd.Start()
|
|
}
|
|
|
|
func withNetworkIsolation(f func() error, _ *drivers.NetworkIsolationSpec) error {
|
|
return f()
|
|
}
|
|
|
|
func setCmdUser(cmd *exec.Cmd, user string) error {
|
|
if user == "" {
|
|
return nil
|
|
}
|
|
nameParts := strings.Split(user, "\\")
|
|
if len(nameParts) != 2 {
|
|
return errors.New("user name must contain domain")
|
|
}
|
|
token, err := createUserToken(nameParts[0], nameParts[1])
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create user token: %w", err)
|
|
}
|
|
|
|
if cmd.SysProcAttr == nil {
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{}
|
|
}
|
|
cmd.SysProcAttr.Token = *token
|
|
|
|
runtime.AddCleanup(cmd, func(attr *syscall.SysProcAttr) {
|
|
_ = attr.Token.Close()
|
|
}, cmd.SysProcAttr)
|
|
|
|
return nil
|
|
}
|
|
|
|
var (
|
|
advapiDll = windows.NewLazySystemDLL("advapi32.dll")
|
|
procLogonUserW = advapiDll.NewProc("LogonUserW")
|
|
)
|
|
|
|
const (
|
|
_LOGON_SERVICE uint32 = 5
|
|
_PROVIDER_DEFAULT uint32 = 0
|
|
)
|
|
|
|
func createUserToken(domain, username string) (*syscall.Token, error) {
|
|
userw, err := syscall.UTF16PtrFromString(username)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to convert username to UTF-16: %w", err)
|
|
}
|
|
domainw, err := syscall.UTF16PtrFromString(domain)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to convert user domain to UTF-16: %w", err)
|
|
}
|
|
var token syscall.Token
|
|
ret, _, e := procLogonUserW.Call(
|
|
uintptr(unsafe.Pointer(userw)),
|
|
uintptr(unsafe.Pointer(domainw)),
|
|
uintptr(unsafe.Pointer(nil)),
|
|
uintptr(_LOGON_SERVICE),
|
|
uintptr(_PROVIDER_DEFAULT),
|
|
uintptr(unsafe.Pointer(&token)),
|
|
)
|
|
if ret == 0 {
|
|
return nil, e
|
|
}
|
|
|
|
return &token, nil
|
|
}
|
|
|
|
func (e *UniversalExecutor) ListProcesses() set.Collection[int] {
|
|
return procstats.ListByPid(e.childCmd.Process.Pid)
|
|
}
|
|
|
|
func (e *UniversalExecutor) setSubCmdCgroup(*exec.Cmd, string) (func(), error) {
|
|
return func() {}, nil
|
|
}
|
|
|
|
// configure new process group for child process and creates a JobObject for the
|
|
// executor. Children of the executor will be created in the same JobObject
|
|
// Ref: https://learn.microsoft.com/en-us/windows/win32/procthread/job-objects
|
|
func (e *UniversalExecutor) setNewProcessGroup() error {
|
|
// We need to check that as build flags includes windows for this file
|
|
if e.childCmd.SysProcAttr == nil {
|
|
e.childCmd.SysProcAttr = &syscall.SysProcAttr{}
|
|
}
|
|
e.childCmd.SysProcAttr.CreationFlags = syscall.CREATE_NEW_PROCESS_GROUP
|
|
|
|
// note: we don't call CloseHandle on this job handle because we need to
|
|
// hold onto it until the executor exits
|
|
job, err := windows.CreateJobObject(nil, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("could not create Windows job object for executor: %w", err)
|
|
}
|
|
|
|
info := windows.JOBOBJECT_EXTENDED_LIMIT_INFORMATION{
|
|
BasicLimitInformation: windows.JOBOBJECT_BASIC_LIMIT_INFORMATION{
|
|
LimitFlags: windows.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
|
|
},
|
|
}
|
|
_, err = windows.SetInformationJobObject(
|
|
job,
|
|
windows.JobObjectExtendedLimitInformation,
|
|
uintptr(unsafe.Pointer(&info)),
|
|
uint32(unsafe.Sizeof(info)))
|
|
if err != nil {
|
|
return fmt.Errorf("could not configure Windows job object for executor: %w", err)
|
|
}
|
|
|
|
handle := windows.CurrentProcess()
|
|
err = windows.AssignProcessToJobObject(job, handle)
|
|
if err != nil {
|
|
return fmt.Errorf("could not assign executor to Windows job object: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Cleanup any still hanging user processes
|
|
func (e *UniversalExecutor) killProcessTree(proc *os.Process) error {
|
|
// We must first verify if the process is still running.
|
|
// (Windows process often lingered around after being reported as killed).
|
|
handle, err := syscall.OpenProcess(syscall.PROCESS_TERMINATE|syscall.SYNCHRONIZE|syscall.PROCESS_QUERY_INFORMATION, false, uint32(proc.Pid))
|
|
if err != nil {
|
|
return os.NewSyscallError("OpenProcess", err)
|
|
}
|
|
defer syscall.CloseHandle(handle)
|
|
|
|
result, err := syscall.WaitForSingleObject(syscall.Handle(handle), 0)
|
|
|
|
switch result {
|
|
case syscall.WAIT_OBJECT_0:
|
|
return nil
|
|
case syscall.WAIT_TIMEOUT:
|
|
// Process still running. Just kill it.
|
|
return proc.Kill()
|
|
default:
|
|
return os.NewSyscallError("WaitForSingleObject", err)
|
|
}
|
|
}
|
|
|
|
// Send a Ctrl-Break signal for shutting down the process,
|
|
func sendCtrlBreak(pid int) error {
|
|
err := windows.GenerateConsoleCtrlEvent(syscall.CTRL_BREAK_EVENT, uint32(pid))
|
|
if err != nil {
|
|
return fmt.Errorf("Error sending ctrl-break event: %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Send the process a Ctrl-Break event, allowing it to shutdown by itself
|
|
// before being Terminate.
|
|
func (e *UniversalExecutor) shutdownProcess(_ os.Signal, proc *os.Process) error {
|
|
if err := sendCtrlBreak(proc.Pid); err != nil {
|
|
return fmt.Errorf("executor shutdown error: %v", err)
|
|
}
|
|
e.logger.Debug("sent Ctrl-Break to process", "pid", proc.Pid)
|
|
|
|
return nil
|
|
}
|